Save generated previews in AppWidgetService
This change updates AppWidgetServiceImpl to persist generated
previews in /data/system_ce/<user>/appwidget/previews. Each file
contains the previews for a single provider, written as a
GeneratedPreviewsProto message.
Previews are cleared when a provider app is updated, deleted
(PACKAGE_REMOVED) or its data is cleared (PACKAGE_DATA_CLEARED).
Also updates the bug for the feature flag to the correct number.
Test: Manual, set previews and reboot, then clear data and remove
package.
Test: AppWidgetTest#testGeneratedPreviewPersistence
Bug: 364420494
Flag: android.appwidget.flags.remote_views_proto
Change-Id: I7c800eeab84480675514e236cac06fe3deb43fac
diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig
index ac9263c..3488b16 100644
--- a/core/java/android/appwidget/flags.aconfig
+++ b/core/java/android/appwidget/flags.aconfig
@@ -55,7 +55,7 @@
name: "remote_views_proto"
namespace: "app_widgets"
description: "Enable support for persisting RemoteViews previews to Protobuf"
- bug: "306546610"
+ bug: "364420494"
}
flag {
diff --git a/core/proto/android/service/appwidget.proto b/core/proto/android/service/appwidget.proto
index 97350ef..fb90719 100644
--- a/core/proto/android/service/appwidget.proto
+++ b/core/proto/android/service/appwidget.proto
@@ -20,6 +20,8 @@
option java_multiple_files = true;
option java_outer_classname = "AppWidgetServiceProto";
+import "frameworks/base/core/proto/android/widget/remoteviews.proto";
+
// represents the object holding the dump info of the app widget service
message AppWidgetServiceDumpProto {
repeated WidgetProto widgets = 1; // the array of bound widgets
@@ -38,3 +40,14 @@
optional int32 maxHeight = 9;
optional bool restoreCompleted = 10;
}
+
+// represents a set of widget previews for a particular provider
+message GeneratedPreviewsProto {
+ repeated Preview previews = 1;
+
+ // represents a particular RemoteViews preview, which may be set for multiple categories
+ message Preview {
+ repeated int32 widget_categories = 1;
+ optional android.widget.RemoteViewsProto views = 2;
+ }
+}
\ No newline at end of file
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index f6ac706..35998d9a 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -17,6 +17,7 @@
package com.android.server.appwidget;
import static android.appwidget.flags.Flags.remoteAdapterConversion;
+import static android.appwidget.flags.Flags.remoteViewsProto;
import static android.appwidget.flags.Flags.removeAppWidgetServiceIoFromCriticalPath;
import static android.appwidget.flags.Flags.securityPolicyInteractAcrossUsers;
import static android.appwidget.flags.Flags.supportResumeRestoreAfterReboot;
@@ -31,6 +32,7 @@
import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.PermissionName;
@@ -104,6 +106,7 @@
import android.os.UserManager;
import android.provider.DeviceConfig;
import android.service.appwidget.AppWidgetServiceDumpProto;
+import android.service.appwidget.GeneratedPreviewsProto;
import android.service.appwidget.WidgetProto;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -122,7 +125,9 @@
import android.util.SparseLongArray;
import android.util.TypedValue;
import android.util.Xml;
+import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
+import android.util.proto.ProtoUtils;
import android.view.Display;
import android.view.View;
import android.widget.RemoteViews;
@@ -134,6 +139,7 @@
import com.android.internal.appwidget.IAppWidgetHost;
import com.android.internal.appwidget.IAppWidgetService;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
+import com.android.internal.infra.AndroidFuture;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.ArrayUtils;
@@ -221,6 +227,10 @@
// XML attribute for widget ids that are pending deletion.
// See {@link Provider#pendingDeletedWidgetIds}.
private static final String PENDING_DELETED_IDS_ATTR = "pending_deleted_ids";
+ // Name of service directory in /data/system_ce/<user>/
+ private static final String APPWIDGET_CE_DATA_DIRNAME = "appwidget";
+ // Name of previews directory in /data/system_ce/<user>/appwidget/
+ private static final String WIDGET_PREVIEWS_DIRNAME = "previews";
// Hard limit of number of hosts an app can create, note that the app that hosts the widgets
// can have multiple instances of {@link AppWidgetHost}, typically in respect to different
@@ -316,6 +326,9 @@
// Handler to the background thread that saves states to disk.
private Handler mSaveStateHandler;
+ // Handler to the background thread that saves generated previews to disk. All operations that
+ // modify saved previews must be run on this Handler.
+ private Handler mSavePreviewsHandler;
// Handler to the foreground thread that handles broadcasts related to user
// and package events, as well as various internal events within
// AppWidgetService.
@@ -359,6 +372,7 @@
} else {
mSaveStateHandler = BackgroundThread.getHandler();
}
+ mSavePreviewsHandler = new Handler(BackgroundThread.get().getLooper());
final ServiceThread serviceThread = new ServiceThread(TAG,
android.os.Process.THREAD_PRIORITY_FOREGROUND, false /* allowIo */);
serviceThread.start();
@@ -378,7 +392,9 @@
SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_MAX_PROVIDERS,
DEFAULT_GENERATED_PREVIEW_MAX_PROVIDERS);
mGeneratedPreviewsApiCounter = new ApiCounter(generatedPreviewResetInterval,
- generatedPreviewMaxCallsPerInterval, generatedPreviewsMaxProviders);
+ generatedPreviewMaxCallsPerInterval,
+ // Set a limit on the number of providers if storing them in memory.
+ remoteViewsProto() ? Integer.MAX_VALUE : generatedPreviewsMaxProviders);
DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_SYSTEMUI,
new HandlerExecutor(mCallbackHandler), this::handleSystemUiDeviceConfigChange);
@@ -644,7 +660,14 @@
for (int i = 0; i < providerCount; i++) {
Provider provider = mProviders.get(i);
if (provider.id.uid == clearedUid) {
- changed |= provider.clearGeneratedPreviewsLocked();
+ if (remoteViewsProto()) {
+ changed |= clearGeneratedPreviewsAsync(provider);
+ } else {
+ changed |= provider.clearGeneratedPreviewsLocked();
+ }
+ if (DEBUG) {
+ Slog.e(TAG, "clearPreviewsForUidLocked " + provider + " changed " + changed);
+ }
}
}
return changed;
@@ -3246,6 +3269,9 @@
deleteWidgetsLocked(provider, UserHandle.USER_ALL);
mProviders.remove(provider);
mGeneratedPreviewsApiCounter.remove(provider.id);
+ if (remoteViewsProto()) {
+ clearGeneratedPreviewsAsync(provider);
+ }
// no need to send the DISABLE broadcast, since the receiver is gone anyway
cancelBroadcastsLocked(provider);
@@ -3824,6 +3850,14 @@
} catch (IOException e) {
Slog.w(TAG, "Failed to read state: " + e);
}
+
+ if (remoteViewsProto()) {
+ try {
+ loadGeneratedPreviewCategoriesLocked(profileId);
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed to read preview categories: " + e);
+ }
+ }
}
if (version >= 0) {
@@ -4593,6 +4627,12 @@
keep.add(providerId);
// Use the new AppWidgetProviderInfo.
provider.setPartialInfoLocked(info);
+ // Clear old previews
+ if (remoteViewsProto()) {
+ clearGeneratedPreviewsAsync(provider);
+ } else {
+ provider.clearGeneratedPreviewsLocked();
+ }
// If it's enabled
final int M = provider.widgets.size();
if (M > 0) {
@@ -4884,6 +4924,7 @@
mSecurityPolicy.enforceCallFromPackage(callingPackage);
ensureWidgetCategoryCombinationIsValid(widgetCategory);
+ AndroidFuture<RemoteViews> result = null;
synchronized (mLock) {
ensureGroupStateLoadedLocked(profileId);
final int providerCount = mProviders.size();
@@ -4917,10 +4958,23 @@
callingPackage);
if (providerIsInCallerProfile && !shouldFilterAppAccess
&& (providerIsInCallerPackage || hasBindAppWidgetPermission)) {
- return provider.getGeneratedPreviewLocked(widgetCategory);
+ if (remoteViewsProto()) {
+ result = getGeneratedPreviewsAsync(provider, widgetCategory);
+ } else {
+ return provider.getGeneratedPreviewLocked(widgetCategory);
+ }
}
}
}
+
+ if (result != null) {
+ try {
+ return result.get();
+ } catch (Exception e) {
+ Slog.e(TAG, "Failed to get generated previews Future result", e);
+ return null;
+ }
+ }
// Either the provider does not exist or the caller does not have permission to access its
// previews.
return null;
@@ -4950,8 +5004,12 @@
providerComponent + " is not a valid AppWidget provider");
}
if (mGeneratedPreviewsApiCounter.tryApiCall(providerId)) {
- provider.setGeneratedPreviewLocked(widgetCategories, preview);
- scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+ if (remoteViewsProto()) {
+ setGeneratedPreviewsAsync(provider, widgetCategories, preview);
+ } else {
+ provider.setGeneratedPreviewLocked(widgetCategories, preview);
+ scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+ }
return true;
}
return false;
@@ -4979,11 +5037,361 @@
throw new IllegalArgumentException(
providerComponent + " is not a valid AppWidget provider");
}
- final boolean changed = provider.removeGeneratedPreviewLocked(widgetCategories);
- if (changed) scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+
+ if (remoteViewsProto()) {
+ removeGeneratedPreviewsAsync(provider, widgetCategories);
+ } else {
+ final boolean changed = provider.removeGeneratedPreviewLocked(widgetCategories);
+ if (changed) scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+ }
}
}
+ /**
+ * Return previews for the specified provider from a background thread. The result of the future
+ * is nullable.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ @NonNull
+ private AndroidFuture<RemoteViews> getGeneratedPreviewsAsync(
+ @NonNull Provider provider, @AppWidgetProviderInfo.CategoryFlags int widgetCategory) {
+ AndroidFuture<RemoteViews> result = new AndroidFuture<>();
+ mSavePreviewsHandler.post(() -> {
+ SparseArray<RemoteViews> previews = loadGeneratedPreviews(provider);
+ for (int i = 0; i < previews.size(); i++) {
+ if ((widgetCategory & previews.keyAt(i)) != 0) {
+ result.complete(previews.valueAt(i));
+ return;
+ }
+ }
+ result.complete(null);
+ });
+ return result;
+ }
+
+ /**
+ * Set previews for the specified provider on a background thread.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ private void setGeneratedPreviewsAsync(@NonNull Provider provider, int widgetCategories,
+ @NonNull RemoteViews preview) {
+ mSavePreviewsHandler.post(() -> {
+ SparseArray<RemoteViews> previews = loadGeneratedPreviews(provider);
+ for (int flag : Provider.WIDGET_CATEGORY_FLAGS) {
+ if ((widgetCategories & flag) != 0) {
+ previews.put(flag, preview);
+ }
+ }
+ saveGeneratedPreviews(provider, previews, /* notify= */ true);
+ });
+ }
+
+ /**
+ * Remove previews for the specified provider on a background thread.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ private void removeGeneratedPreviewsAsync(@NonNull Provider provider, int widgetCategories) {
+ mSavePreviewsHandler.post(() -> {
+ SparseArray<RemoteViews> previews = loadGeneratedPreviews(provider);
+ boolean changed = false;
+ for (int flag : Provider.WIDGET_CATEGORY_FLAGS) {
+ if ((widgetCategories & flag) != 0) {
+ changed |= previews.removeReturnOld(flag) != null;
+ }
+ }
+ if (changed) {
+ saveGeneratedPreviews(provider, previews, /* notify= */ true);
+ }
+ });
+ }
+
+ /**
+ * Clear previews for the specified provider on a background thread. Returns true if changed
+ * (i.e. there are previews to clear). If returns true, the caller should schedule a providers
+ * changed notification.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ private boolean clearGeneratedPreviewsAsync(@NonNull Provider provider) {
+ mSavePreviewsHandler.post(() -> {
+ saveGeneratedPreviews(provider, /* previews= */ null, /* notify= */ false);
+ });
+ return provider.info.generatedPreviewCategories != 0;
+ }
+
+ private void checkSavePreviewsThread() {
+ if (DEBUG && !mSavePreviewsHandler.getLooper().isCurrentThread()) {
+ throw new IllegalStateException("Only modify previews on the background thread");
+ }
+ }
+
+ /**
+ * Load previews from file for the given provider. If there are no previews, returns an empty
+ * SparseArray. Else, returns a SparseArray of the previews mapped by widget category.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ @NonNull
+ private SparseArray<RemoteViews> loadGeneratedPreviews(@NonNull Provider provider) {
+ checkSavePreviewsThread();
+ try {
+ AtomicFile previewsFile = getWidgetPreviewsFile(provider);
+ if (!previewsFile.exists()) {
+ return new SparseArray<>();
+ }
+ ProtoInputStream input = new ProtoInputStream(previewsFile.readFully());
+ SparseArray<RemoteViews> entries = readGeneratedPreviewsFromProto(input);
+ SparseArray<RemoteViews> singleCategoryKeyedEntries = new SparseArray<>();
+ for (int i = 0; i < entries.size(); i++) {
+ int widgetCategories = entries.keyAt(i);
+ RemoteViews preview = entries.valueAt(i);
+ for (int flag : Provider.WIDGET_CATEGORY_FLAGS) {
+ if ((widgetCategories & flag) != 0) {
+ singleCategoryKeyedEntries.put(flag, preview);
+ }
+ }
+ }
+ return singleCategoryKeyedEntries;
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to load generated previews for " + provider, e);
+ return new SparseArray<>();
+ }
+ }
+
+ /**
+ * This is called when loading profile/group state to populate
+ * AppWidgetProviderInfo.generatedPreviewCategories based on what previews are saved.
+ *
+ * This is the only time previews are read while not on mSavePreviewsHandler. It happens once
+ * per profile during initialization, before any calls to get/set/removeWidgetPreviewAsync
+ * happen for that profile.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ @GuardedBy("mLock")
+ private void loadGeneratedPreviewCategoriesLocked(int profileId) throws IOException {
+ for (Provider provider : mProviders) {
+ if (provider.id.getProfile().getIdentifier() != profileId) {
+ continue;
+ }
+ AtomicFile previewsFile = getWidgetPreviewsFile(provider);
+ if (!previewsFile.exists()) {
+ continue;
+ }
+ ProtoInputStream input = new ProtoInputStream(previewsFile.readFully());
+ provider.info.generatedPreviewCategories = readGeneratedPreviewCategoriesFromProto(
+ input);
+ if (DEBUG) {
+ Slog.i(TAG, TextUtils.formatSimple(
+ "loadGeneratedPreviewCategoriesLocked %d %s categories %d", profileId,
+ provider, provider.info.generatedPreviewCategories));
+ }
+ }
+ }
+
+ /**
+ * Save the given previews into storage.
+ *
+ * @param provider Provider for which to save previews
+ * @param previews Previews to save. If null or empty, clears any saved previews for this
+ * provider.
+ * @param notify If true, then this function will notify hosts of updated provider info.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ private void saveGeneratedPreviews(@NonNull Provider provider,
+ @Nullable SparseArray<RemoteViews> previews, boolean notify) {
+ checkSavePreviewsThread();
+ AtomicFile file = null;
+ FileOutputStream stream = null;
+ try {
+ file = getWidgetPreviewsFile(provider);
+ if (previews == null || previews.size() == 0) {
+ if (file.exists()) {
+ if (DEBUG) {
+ Slog.i(TAG, "Deleting widget preview file " + file);
+ }
+ file.delete();
+ }
+ } else {
+ if (DEBUG) {
+ Slog.i(TAG, "Writing widget preview file " + file);
+ }
+ ProtoOutputStream out = new ProtoOutputStream();
+ writePreviewsToProto(out, previews);
+ stream = file.startWrite();
+ stream.write(out.getBytes());
+ file.finishWrite(stream);
+ }
+
+ synchronized (mLock) {
+ provider.updateGeneratedPreviewCategoriesLocked(previews);
+ if (notify) {
+ scheduleNotifyGroupHostsForProvidersChangedLocked(provider.getUserId());
+ }
+ }
+ } catch (IOException e) {
+ if (file != null && stream != null) {
+ file.failWrite(stream);
+ }
+ Slog.w(TAG, "Failed to save widget previews for provider " + provider.id.componentName);
+ }
+ }
+
+
+ /**
+ * Write the given previews as a GeneratedPreviewsProto to the output stream.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ private void writePreviewsToProto(@NonNull ProtoOutputStream out,
+ @NonNull SparseArray<RemoteViews> generatedPreviews) {
+ // Collect RemoteViews mapped by hashCode in order to avoid writing duplicates.
+ SparseArray<Pair<Integer, RemoteViews>> previewsToWrite = new SparseArray<>();
+ for (int i = 0; i < generatedPreviews.size(); i++) {
+ int widgetCategory = generatedPreviews.keyAt(i);
+ RemoteViews views = generatedPreviews.valueAt(i);
+ if (!previewsToWrite.contains(views.hashCode())) {
+ previewsToWrite.put(views.hashCode(), new Pair<>(widgetCategory, views));
+ } else {
+ Pair<Integer, RemoteViews> entry = previewsToWrite.get(views.hashCode());
+ previewsToWrite.put(views.hashCode(),
+ Pair.create(entry.first | widgetCategory, views));
+ }
+ }
+
+ for (int i = 0; i < previewsToWrite.size(); i++) {
+ final long token = out.start(GeneratedPreviewsProto.PREVIEWS);
+ Pair<Integer, RemoteViews> entry = previewsToWrite.valueAt(i);
+ out.write(GeneratedPreviewsProto.Preview.WIDGET_CATEGORIES, entry.first);
+ final long viewsToken = out.start(GeneratedPreviewsProto.Preview.VIEWS);
+ entry.second.writePreviewToProto(mContext, out);
+ out.end(viewsToken);
+ out.end(token);
+ }
+ }
+
+ /**
+ * Read a GeneratedPreviewsProto message from the input stream.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ @NonNull
+ private SparseArray<RemoteViews> readGeneratedPreviewsFromProto(@NonNull ProtoInputStream input)
+ throws IOException {
+ SparseArray<RemoteViews> entries = new SparseArray<>();
+ while (input.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ switch (input.getFieldNumber()) {
+ case (int) GeneratedPreviewsProto.PREVIEWS:
+ final long token = input.start(GeneratedPreviewsProto.PREVIEWS);
+ Pair<Integer, RemoteViews> entry = readSinglePreviewFromProto(input,
+ /* skipViews= */ false);
+ entries.put(entry.first, entry.second);
+ input.end(token);
+ break;
+ default:
+ Slog.w(TAG, "Unknown field while reading GeneratedPreviewsProto! "
+ + ProtoUtils.currentFieldToString(input));
+ }
+ }
+ return entries;
+ }
+
+ /**
+ * Read the widget categories from GeneratedPreviewsProto and return an int representing the
+ * combined widget categories of all the previews.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ @AppWidgetProviderInfo.CategoryFlags
+ private int readGeneratedPreviewCategoriesFromProto(@NonNull ProtoInputStream input)
+ throws IOException {
+ int widgetCategories = 0;
+ while (input.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ switch (input.getFieldNumber()) {
+ case (int) GeneratedPreviewsProto.PREVIEWS:
+ final long token = input.start(GeneratedPreviewsProto.PREVIEWS);
+ Pair<Integer, RemoteViews> entry = readSinglePreviewFromProto(input,
+ /* skipViews= */ true);
+ widgetCategories |= entry.first;
+ input.end(token);
+ break;
+ default:
+ Slog.w(TAG, "Unknown field while reading GeneratedPreviewsProto! "
+ + ProtoUtils.currentFieldToString(input));
+ }
+ }
+ return widgetCategories;
+ }
+
+ /**
+ * Read a single GeneratedPreviewsProto.Preview message from the input stream, and returns a
+ * pair of widget category and corresponding RemoteViews. If skipViews is true, this function
+ * will only read widget categories and the returned RemoteViews will be null.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ @NonNull
+ private Pair<Integer, RemoteViews> readSinglePreviewFromProto(@NonNull ProtoInputStream input,
+ boolean skipViews) throws IOException {
+ int widgetCategories = 0;
+ RemoteViews views = null;
+ while (input.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ switch (input.getFieldNumber()) {
+ case (int) GeneratedPreviewsProto.Preview.VIEWS:
+ if (skipViews) {
+ // ProtoInputStream will skip over the nested message when nextField() is
+ // called.
+ continue;
+ }
+ final long token = input.start(GeneratedPreviewsProto.Preview.VIEWS);
+ try {
+ views = RemoteViews.createPreviewFromProto(mContext, input);
+ } catch (Exception e) {
+ Slog.e(TAG, "Unable to deserialize RemoteViews", e);
+ }
+ input.end(token);
+ break;
+ case (int) GeneratedPreviewsProto.Preview.WIDGET_CATEGORIES:
+ widgetCategories = input.readInt(
+ GeneratedPreviewsProto.Preview.WIDGET_CATEGORIES);
+ break;
+ default:
+ Slog.w(TAG, "Unknown field while reading GeneratedPreviewsProto! "
+ + ProtoUtils.currentFieldToString(input));
+ }
+ }
+ return Pair.create(widgetCategories, views);
+ }
+
+ /**
+ * Returns the file in which all generated previews for this provider are stored. This will be
+ * a path of the form:
+ * {@literal /data/system_ce/<userId>/appwidget/previews/<package>-<class>-<uid>.binpb}
+ *
+ * This function will not create the file if it does not already exist.
+ */
+ @NonNull
+ private static AtomicFile getWidgetPreviewsFile(@NonNull Provider provider) throws IOException {
+ int userId = provider.getUserId();
+ File previewsDirectory = getWidgetPreviewsDirectory(userId);
+ File providerPreviews = Environment.buildPath(previewsDirectory,
+ TextUtils.formatSimple("%s-%s-%d.binpb", provider.id.componentName.getPackageName(),
+ provider.id.componentName.getClassName(), provider.id.uid));
+ return new AtomicFile(providerPreviews);
+ }
+
+ /**
+ * Returns the widget previews directory for the given user, creating it if it does not exist.
+ * This will be a path of the form:
+ * {@literal /data/system_ce/<userId>/appwidget/previews}
+ */
+ @NonNull
+ private static File getWidgetPreviewsDirectory(int userId) throws IOException {
+ File dataSystemCeDirectory = Environment.getDataSystemCeDirectory(userId);
+ File previewsDirectory = Environment.buildPath(dataSystemCeDirectory,
+ APPWIDGET_CE_DATA_DIRNAME, WIDGET_PREVIEWS_DIRNAME);
+ if (!previewsDirectory.exists()) {
+ if (!previewsDirectory.mkdirs()) {
+ throw new IOException("Unable to create widget preview directory "
+ + previewsDirectory.getPath());
+ }
+ }
+ return previewsDirectory;
+ }
+
private static void ensureWidgetCategoryCombinationIsValid(int widgetCategories) {
int validCategories = AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
| AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD
@@ -5415,11 +5823,11 @@
AppWidgetManager.META_DATA_APPWIDGET_PROVIDER);
}
if (newInfo != null) {
+ newInfo.generatedPreviewCategories = info.generatedPreviewCategories;
info = newInfo;
if (DEBUG) {
Objects.requireNonNull(info);
}
- updateGeneratedPreviewCategoriesLocked();
}
}
mInfoParsed = true;
@@ -5476,7 +5884,7 @@
generatedPreviews.put(flag, preview);
}
}
- updateGeneratedPreviewCategoriesLocked();
+ updateGeneratedPreviewCategoriesLocked(generatedPreviews);
}
@GuardedBy("this.mLock")
@@ -5488,7 +5896,7 @@
}
}
if (changed) {
- updateGeneratedPreviewCategoriesLocked();
+ updateGeneratedPreviewCategoriesLocked(generatedPreviews);
}
return changed;
}
@@ -5497,17 +5905,19 @@
public boolean clearGeneratedPreviewsLocked() {
if (generatedPreviews.size() > 0) {
generatedPreviews.clear();
- updateGeneratedPreviewCategoriesLocked();
+ updateGeneratedPreviewCategoriesLocked(generatedPreviews);
return true;
}
return false;
}
-
@GuardedBy("this.mLock")
- private void updateGeneratedPreviewCategoriesLocked() {
+ private void updateGeneratedPreviewCategoriesLocked(
+ @Nullable SparseArray<RemoteViews> previews) {
info.generatedPreviewCategories = 0;
- for (int i = 0; i < generatedPreviews.size(); i++) {
- info.generatedPreviewCategories |= generatedPreviews.keyAt(i);
+ if (previews != null) {
+ for (int i = 0; i < previews.size(); i++) {
+ info.generatedPreviewCategories |= previews.keyAt(i);
+ }
}
}