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

Commit 99b314af authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Fixing indefinite list loading with legacy list conversion" into main

parents ec1e27a4 8006ff70
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