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

Commit 8006ff70 authored by Sunny Goyal's avatar Sunny Goyal
Browse files

Fixing indefinite list loading with legacy list conversion

> Sometimes apps can push remoteViews even before the widget conversation with the assumption that if will not be displayed until configuration completes.
  The widget service now maintains a flag for completed widget configuration, and disables legacy list conversion, until that.
> Simulating legacy remote-list- service behavior, onDataSetChanged is only called the first time, or if explicitely called.
> Not ignoring errors on binder call, instead passing them on to the caller
> Preventing thread straving, by actually canceling the task instead of leaving it running and just cancelling the result.

Bug: 417913555
Flag: android.appwidget.flags.remote_adapter_conversion
Test: atest RemoteViewsTest; atest AppWidgetServiceImplTest
Change-Id: I89ded0ce376d929193dbf23367b0aa7760e5052c
parent b9811e06
Loading
Loading
Loading
Loading
+87 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 android.appwidget;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowManagerWrapper;

import androidx.annotation.Nullable;

/**
 * Activity to proxy config activity launches
 *
 * @hide
 */
public class AppWidgetConfigActivityProxy extends Activity {

    private static final int CONFIG_ACTIVITY_REQUEST_CODE = 1;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setResult(RESULT_CANCELED);
        Intent intent = getIntent();
        Intent target = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent.class);
        if (target == null) {
            finish();
            return;
        }

        startActivityForResult(target, CONFIG_ACTIVITY_REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        setResult(resultCode, data);
        int widgetId = getIntent().getIntExtra(
                AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
        AppWidgetManager.getInstance(this).setConfigActivityComplete(widgetId);

        finish();
    }

    @Override
    public WindowManager getWindowManager() {
        return new MyWM(super.getWindowManager());
    }

    /** Wrapper over windowManager with disables adding a window */
    private static class MyWM extends WindowManagerWrapper {

        MyWM(WindowManager original) {
            super(original);
        }

        @Override
        public void addView(View view, ViewGroup.LayoutParams params) { }

        @Override
        public void updateViewLayout(View view, ViewGroup.LayoutParams params) { }

        @Override
        public void removeView(View view) { }

        @Override
        public void removeViewImmediate(View view) { }
    }
}
+159 −55
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.annotation.SystemService;
import android.annotation.TestApi;
import android.annotation.UiThread;
import android.annotation.UserIdInt;
import android.annotation.WorkerThread;
import android.app.IServiceConnection;
import android.app.PendingIntent;
import android.app.usage.UsageStatsManager;
@@ -47,8 +48,6 @@ import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Process;
@@ -57,7 +56,6 @@ import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pair;
import android.widget.RemoteViews;

import com.android.internal.appwidget.IAppWidgetService;
@@ -65,13 +63,14 @@ import com.android.internal.os.BackgroundThread;
import com.android.internal.util.FunctionalUtils;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Consumer;

/**
@@ -583,6 +582,13 @@ public class AppWidgetManager {
     */
    public static final String EXTRA_APPWIDGET_PREVIEW = "appWidgetPreview";

    /**
     * The maximum waiting time for remote adapter conversion in milliseconds
     *
     * @hide
     */
    private static final long MAX_ADAPTER_CONVERSION_WAITING_TIME_MS = 20_000;

    /**
     * Field for the manifest meta-data tag.
     *
@@ -600,7 +606,8 @@ public class AppWidgetManager {

    private boolean mHasPostedLegacyLists = false;

    private @NonNull ServiceCollectionCache mServiceCollectionCache;
    @NonNull
    private final ServiceCollectionCache mServiceCollectionCache;

    /**
     * Get the AppWidgetManager instance to use for the supplied {@link android.content.Context
@@ -653,25 +660,40 @@ public class AppWidgetManager {

    private void tryAdapterConversion(
            FunctionalUtils.RemoteExceptionIgnoringConsumer<RemoteViews> action,
            RemoteViews original, String failureMsg) {
        if (remoteAdapterConversion()
                && (mHasPostedLegacyLists = mHasPostedLegacyLists
                        || (original != null && original.hasLegacyLists()))) {
            RemoteViews original, int[] appWidgetIds, String failureMsg) {
        if (remoteAdapterConversion()) {
            mHasPostedLegacyLists = mHasPostedLegacyLists
                    || (original != null && original.hasLegacyLists());
        }
        if (remoteAdapterConversion() && mHasPostedLegacyLists && original != null) {
            final RemoteViews viewsCopy = new RemoteViews(original);
            Runnable updateWidgetWithTask = () -> {
                try {
                    viewsCopy.collectAllIntents(mMaxBitmapMemory, mServiceCollectionCache).get();
                    if (shouldSkipListConversion(viewsCopy, appWidgetIds)) {
                        Log.d(TAG, "Skipping legacy list conversion, pending config activity");
                        viewsCopy.replaceAllIntentsWithEmptyList();
                    } else {
                        viewsCopy.collectAllIntents(mMaxBitmapMemory, false /* invalidateData */,
                                        mServiceCollectionCache)
                                .thenRun(() -> {
                                    try {
                                        action.acceptOrThrow(viewsCopy);
                                    } catch (RemoteException e) {
                                        e.rethrowFromSystemServer();
                                    }
                                }).exceptionally(e -> {
                                    Log.e(TAG, failureMsg, e);
                                    return null;
                                });
                    }
                } catch (Exception e) {
                    Log.e(TAG, failureMsg, e);
                }
            };

            if (Looper.getMainLooper() == Looper.myLooper()) {
                createUpdateExecutorIfNull().execute(updateWidgetWithTask);
                return;
            }

            updateWidgetWithTask.run();
        } else {
            try {
@@ -682,6 +704,13 @@ public class AppWidgetManager {
        }
    }

    private boolean shouldSkipListConversion(RemoteViews views, int[] appWidgetIds)
            throws RemoteException {
        return appWidgetIds.length == 1
                && views.hasLegacyLists()
                && mService.isFirstConfigActivityPending(mPackageName, appWidgetIds[0]);
    }

    /**
     * Set the RemoteViews to use for the specified appWidgetIds.
     * <p>
@@ -707,7 +736,7 @@ public class AppWidgetManager {
        }

        tryAdapterConversion(view -> mService.updateAppWidgetIds(mPackageName, appWidgetIds,
                view), views, "Error updating app widget views in background");
                view), views, appWidgetIds, "Error updating app widget views in background");
    }

    /**
@@ -813,7 +842,7 @@ public class AppWidgetManager {
        }

        tryAdapterConversion(view -> mService.partiallyUpdateAppWidgetIds(mPackageName,
                appWidgetIds, view), views,
                appWidgetIds, view), views, appWidgetIds,
                "Error partially updating app widget views in background");
    }

@@ -867,7 +896,7 @@ public class AppWidgetManager {
        }

        tryAdapterConversion(view -> mService.updateAppWidgetProvider(provider, view), views,
                "Error updating app widget view using provider in background");
                new int[0], "Error updating app widget view using provider in background");
    }

    /**
@@ -924,10 +953,10 @@ public class AppWidgetManager {
        }

        if (remoteAdapterConversion()) {
            if (Looper.myLooper() == Looper.getMainLooper()) {
            mHasPostedLegacyLists = true;
                createUpdateExecutorIfNull().execute(() -> notifyCollectionWidgetChange(
                        appWidgetIds, viewId));
            if (Looper.myLooper() == Looper.getMainLooper()) {
                createUpdateExecutorIfNull().execute(
                        () -> notifyCollectionWidgetChange(appWidgetIds, viewId));
            } else {
                notifyCollectionWidgetChange(appWidgetIds, viewId);
            }
@@ -940,23 +969,36 @@ public class AppWidgetManager {
        }
    }

    @WorkerThread
    private void notifyCollectionWidgetChange(int[] appWidgetIds, int viewId) {
        try {
            List<CompletableFuture<Void>> updateFutures = new ArrayList<>();
            for (int i = 0; i < appWidgetIds.length; i++) {
                final int widgetId = appWidgetIds[i];
                updateFutures.add(CompletableFuture.runAsync(() -> {
            for (final int widgetId : appWidgetIds) {
                try {
                    if (mService.isFirstConfigActivityPending(mPackageName, widgetId)) {
                        Log.d(TAG, "Skipping collection notify, pending config activity");
                        continue;
                    }
                    RemoteViews views = mService.getAppWidgetViews(mPackageName, widgetId);
                        if (views.replaceRemoteCollections(viewId)) {
                            updateAppWidget(widgetId, views);
                    if (views == null || !views.replaceRemoteCollections(viewId)) {
                        continue;
                    }
                    views.collectAllIntents(
                            mMaxBitmapMemory, true /* invalidateData */, mServiceCollectionCache)
                            .thenRun(() -> {
                                try {
                                    mService.updateAppWidgetIds(
                                            mPackageName, new int[]{widgetId}, views);
                                } catch (RemoteException e) {
                                    e.rethrowFromSystemServer();
                                }
                            }).exceptionally(e -> {
                                Log.e(TAG, "Error notifying changes in RemoteViews", e);
                                return null;
                            });
                } catch (Exception e) {
                    Log.e(TAG, "Error notifying changes in RemoteViews", e);
                }
                }));
            }
            CompletableFuture.allOf(updateFutures.toArray(CompletableFuture[]::new)).join();
        } catch (Exception e) {
            Log.e(TAG, "Error notifying changes for all widgets", e);
        }
@@ -1591,20 +1633,29 @@ public class AppWidgetManager {
        }
    }

    /** @hide */
    public void setConfigActivityComplete(int widgetId) {
        try {
            mService.setConfigActivityComplete(widgetId);
        } catch (RemoteException e) {
            Log.d(TAG, "Error notifying config activity completed");
        }
    }

    @UiThread
    private static @NonNull Executor createUpdateExecutorIfNull() {
        if (sUpdateExecutor == null) {
            sUpdateExecutor = new HandlerExecutor(createAndStartNewHandler(
                    "widget_manager_update_helper_thread", Process.THREAD_PRIORITY_FOREGROUND));
            sUpdateExecutor = createExecutorService(
                    "widget_manager_update_helper_thread", Process.THREAD_PRIORITY_FOREGROUND);
        }

        return sUpdateExecutor;
    }

    private static @NonNull Handler createAndStartNewHandler(@NonNull String name, int priority) {
        HandlerThread thread = new HandlerThread(name, priority);
        thread.start();
        return thread.getThreadHandler();
    private static ExecutorService createExecutorService(@NonNull String name, int priority) {
        return Executors.newSingleThreadExecutor(r -> new Thread(() -> {
            Process.setThreadPriority(Process.myTid(), priority);
            r.run();
        }, name));
    }

    /**
@@ -1629,27 +1680,31 @@ public class AppWidgetManager {
         * Connect to the service indicated by the {@code Intent}, and consume the binder on the
         * specified executor
         */
        public void connectAndConsume(Intent intent, Consumer<IBinder> task, Executor executor) {
            mHandler.post(() -> connectAndConsumeInner(intent, task, executor));
        public void connectAndConsume(Intent intent, Consumer<IBinder> task) {
            mHandler.post(() -> connectAndConsumeInner(intent, task));
        }

        private void connectAndConsumeInner(Intent intent, Consumer<IBinder> task,
                Executor executor) {
        private void connectAndConsumeInner(Intent intent, Consumer<IBinder> task) {
            ConnectionTask activeConnection = mActiveConnections.computeIfAbsent(
                    new FilterComparison(intent), ConnectionTask::new);
            activeConnection.add(task, executor);
            activeConnection.add(task);
        }

        private class ConnectionTask implements ServiceConnection {

            private final Runnable mDestroyAfterTimeout = this::onDestroyTimeout;
            private final ArrayDeque<Pair<Consumer<IBinder>, Executor>> mTaskQueue =
                    new ArrayDeque<>();
            private final Runnable mConnectionTimeout = this::onConnectionTimeout;
            private final ArrayDeque<Consumer<IBinder>> mTaskQueue = new ArrayDeque<>();

            private final String mExecutorName;
            private boolean mOnDestroyTimeout = false;
            private IBinder mIBinder;

            private ExecutorService mBinderCallExecutor;
            private Object mCurrentTaskToken;

            ConnectionTask(@NonNull FilterComparison filter) {
                mExecutorName = "appwidget-connectiontask-" + filter.hashCode();
                try {
                    mContext.bindService(filter.getIntent(),
                            Context.BindServiceFlags.of(Context.BIND_AUTO_CREATE),
@@ -1658,12 +1713,12 @@ public class AppWidgetManager {
                } catch (Exception e) {
                    Log.e(TAG, "Error connecting to service in connection cache", e);
                }
                mHandler.postDelayed(mConnectionTimeout, MAX_ADAPTER_CONVERSION_WAITING_TIME_MS);
            }

            @Override
            public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
                mIBinder = iBinder;
                mHandler.post(this::handleNext);
                onBinderReceived(iBinder);
            }

            @Override
@@ -1672,26 +1727,56 @@ public class AppWidgetManager {
                onServiceConnected(name, new Binder());
            }

            private void onConnectionTimeout() {
                onBinderReceived(new Binder());
            }

            private void onBinderReceived(IBinder iBinder) {
                if (mIBinder != null) {
                    return;
                }
                mIBinder = iBinder;
                mHandler.removeCallbacks(mConnectionTimeout);
                mHandler.post(this::handleNext);
            }

            @Override
            public void onServiceDisconnected(ComponentName componentName) { }

            void add(Consumer<IBinder> task, Executor executor) {
                mTaskQueue.add(Pair.create(task, executor));
            void add(Consumer<IBinder> task) {
                mTaskQueue.add(task);
                if (mOnDestroyTimeout) {
                    // If we are waiting for timeout, cancel it and execute the next task
                    handleNext();
                }
            }

            private void onTaskComplete(Object taskToken) {
                if (mCurrentTaskToken == taskToken) {
                    mCurrentTaskToken = null;
                    handleNext();
                }
            }

            private void handleNext() {
                mHandler.removeCallbacks(mDestroyAfterTimeout);
                Pair<Consumer<IBinder>, Executor> next = mTaskQueue.pollFirst();
                Consumer<IBinder> next = mTaskQueue.pollFirst();
                if (next != null) {
                    mCurrentTaskToken = next;
                    mOnDestroyTimeout = false;
                    next.second.execute(() -> {
                        next.first.accept(mIBinder);
                        mHandler.post(this::handleNext);
                    if (mBinderCallExecutor == null) {
                        mBinderCallExecutor = createExecutorService(
                                mExecutorName, Process.THREAD_PRIORITY_FOREGROUND);
                    }
                    Future task = mBinderCallExecutor.submit(() -> {
                        next.accept(mIBinder);
                        mHandler.post(() -> onTaskComplete(next));
                    });

                    mHandler.postDelayed(
                            () -> onTaskTimeout(task, next),
                            MAX_ADAPTER_CONVERSION_WAITING_TIME_MS);

                } else {
                    // Finished all tasks, start a timeout to unbind this service
                    mOnDestroyTimeout = true;
@@ -1699,6 +1784,22 @@ public class AppWidgetManager {
                }
            }

            /**
             * If a task times out, we try to interrupt it, but also switch to a new executor,
             * in case the previous executor gets blocked for ever
             */
            private void onTaskTimeout(Future task, Object taskToken) {
                if (!task.isDone()) {
                    ExecutorService oldExecutor = mBinderCallExecutor;
                    mBinderCallExecutor = null;
                    task.cancel(true);
                    if (oldExecutor != null) {
                        oldExecutor.shutdown();
                    }
                    onTaskComplete(taskToken);
                }
            }

            /**
             * Called after we have waited for {@link #mTimeOut} after the last task is finished
             */
@@ -1712,6 +1813,9 @@ public class AppWidgetManager {
                } catch (Exception e) {
                    Log.e(TAG, "Error unbinding the cached connection", e);
                }
                if (mBinderCallExecutor != null) {
                    mBinderCallExecutor.shutdown();
                }
                mActiveConnections.values().remove(this);
            }
        }
+36 −40

File changed.

Preview size limit exceeded, changes collapsed.

+6 −7
Original line number Diff line number Diff line
@@ -130,8 +130,6 @@ public abstract class RemoteViewsService extends Service {
         */
        default RemoteViews.RemoteCollectionItems getRemoteCollectionItems(int capSize,
                int capBitmapSize) {
            RemoteViews.RemoteCollectionItems items = new RemoteViews.RemoteCollectionItems
                    .Builder().build();
            Parcel capSizeTestParcel = Parcel.obtain();
            // restore allowSquashing to reduce the noise in error messages
            boolean prevAllowSquashing = capSizeTestParcel.allowSquashing();
@@ -140,7 +138,6 @@ public abstract class RemoteViewsService extends Service {
                RemoteViews.RemoteCollectionItems.Builder itemsBuilder =
                        new RemoteViews.RemoteCollectionItems.Builder();
                RemoteViews.BitmapCache testBitmapCache = null;
                onDataSetChanged();

                itemsBuilder.setHasStableIds(hasStableIds());
                final int numOfEntries = getCount();
@@ -167,14 +164,12 @@ public abstract class RemoteViewsService extends Service {

                    itemsBuilder.addItem(currentItemId, currentView);
                }

                items = itemsBuilder.build();
                return itemsBuilder.build();
            } finally {
                capSizeTestParcel.restoreAllowSquashing(prevAllowSquashing);
                // Recycle the parcel
                capSizeTestParcel.recycle();
            }
            return items;
        }
    }

@@ -282,10 +277,14 @@ public abstract class RemoteViewsService extends Service {

        @Override
        public RemoteViews.RemoteCollectionItems getRemoteCollectionItems(int capSize,
                int capBitmapSize) {
                int capBitmapSize, boolean invalidateData) {
            RemoteViews.RemoteCollectionItems items = new RemoteViews.RemoteCollectionItems
                    .Builder().build();
            try {
                if (mIsCreated || invalidateData) {
                    mFactory.onDataSetChanged();
                    mIsCreated = false;
                }
                items = mFactory.getRemoteCollectionItems(capSize, capBitmapSize);
            } catch (Exception ex) {
                Thread t = Thread.currentThread();
+4 −0
Original line number Diff line number Diff line
@@ -89,5 +89,9 @@ interface IAppWidgetService {
            in ComponentName providerComponent, in int profileId, in int widgetCategory);
    void removeWidgetPreview(in ComponentName providerComponent, in int widgetCategories);
    oneway void reportWidgetEvents(in String callingPackage, in AppWidgetEvent[] events);

    // For legacy list migration
    boolean isFirstConfigActivityPending(in String callingPackage, in int appWidgetId);
    oneway void setConfigActivityComplete(in int appWidgetId);
}
Loading