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

Commit f73e2ac7 authored by Jeff DeCew's avatar Jeff DeCew
Browse files

Use @NotifInflation thread for ExpandableNotificationRow

Change RowInflaterTask to use a new class, AsyncRowInflater which uses coroutines to do the same work implemented by AsyncLayoutInflater, but specifically on the same thread as other notification inflation.  This will hopefully reduce resource cache race conditions.

Bug: 375320642
Flag: EXEMPT bugfix
Test: atest SystemUITests
Change-Id: Id2f18f2b7db313d0e41f997be1e0fc48c05e42a8
parent cd5e9c37
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -983,6 +983,16 @@ flag {
    }
}

flag {
    name: "use_notif_inflation_thread_for_row"
    namespace: "systemui"
    description: "use the @NotifInflation thread for ExpandableNotificationRow inflation"
    bug: "375320642"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "notify_power_manager_user_activity_background"
    namespace: "systemui"
+90 −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 com.android.systemui.statusbar.notification.row

import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.annotation.UiThread
import com.android.app.tracing.coroutines.launchTraced
import com.android.app.tracing.coroutines.withContextTraced
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dagger.qualifiers.NotifInflation
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job

@SysUISingleton
class AsyncRowInflater
@Inject
constructor(
    @Application private val applicationScope: CoroutineScope,
    @Main private val mainCoroutineDispatcher: CoroutineDispatcher,
    @NotifInflation private val inflationCoroutineDispatcher: CoroutineDispatcher,
) {
    /**
     * Inflate the layout on the background thread, and invoke the listener on the main thread when
     * finished.
     *
     * If the inflation fails on the background, it will be retried once on the main thread.
     */
    @UiThread
    fun inflate(
        context: Context,
        layoutFactory: LayoutInflater.Factory2,
        @LayoutRes resId: Int,
        parent: ViewGroup,
        listener: OnInflateFinishedListener,
    ): Job {
        val inflater = BasicRowInflater(context).apply { factory2 = layoutFactory }
        return applicationScope.launchTraced("AsyncRowInflater-bg", inflationCoroutineDispatcher) {
            val view =
                try {
                    inflater.inflate(resId, parent, false)
                } catch (ex: RuntimeException) {
                    // Probably a Looper failure, retry on the UI thread
                    Log.w(
                        "AsyncRowInflater",
                        "Failed to inflate resource in the background!" +
                            " Retrying on the UI thread",
                        ex,
                    )
                    null
                }
            withContextTraced("AsyncRowInflater-ui", mainCoroutineDispatcher) {
                // If the inflate failed on the inflation thread, try again on the main thread
                val finalView = view ?: inflater.inflate(resId, parent, false)
                // Inform the listener of the completion
                listener.onInflateFinished(finalView, resId, parent)
            }
        }
    }

    /**
     * Callback interface (identical to the one from AsyncLayoutInflater) for receiving the inflated
     * view
     */
    interface OnInflateFinishedListener {
        @UiThread fun onInflateFinished(view: View, @LayoutRes resId: Int, parent: ViewGroup?)
    }
}
+51 −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 com.android.systemui.statusbar.notification.row

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View

/**
 * A [LayoutInflater] that is copy of
 * [androidx.asynclayoutinflater.view.AsyncLayoutInflater.BasicInflater]
 */
internal class BasicRowInflater(context: Context) : LayoutInflater(context) {
    override fun cloneInContext(newContext: Context): LayoutInflater {
        return BasicRowInflater(newContext)
    }

    @Throws(ClassNotFoundException::class)
    override fun onCreateView(name: String, attrs: AttributeSet): View {
        for (prefix in sClassPrefixList) {
            try {
                val view = createView(name, prefix, attrs)
                if (view != null) {
                    return view
                }
            } catch (e: ClassNotFoundException) {
                // In this case we want to let the base class take a crack at it.
            }
        }

        return super.onCreateView(name, attrs)
    }

    companion object {
        private val sClassPrefixList = arrayOf("android.widget.", "android.webkit.", "android.app.")
    }
}
+14 −37
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.asynclayoutinflater.view.AsyncLayoutFactory;
import androidx.asynclayoutinflater.view.AsyncLayoutInflater;

import com.android.systemui.Flags;
import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.InflationTask;
@@ -44,7 +45,8 @@ import javax.inject.Inject;
/**
 * An inflater task that asynchronously inflates a ExpandableNotificationRow
 */
public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInflateFinishedListener {
public class RowInflaterTask implements InflationTask,
        AsyncLayoutInflater.OnInflateFinishedListener, AsyncRowInflater.OnInflateFinishedListener {

    private static final String TAG = "RowInflaterTask";
    private static final boolean TRACE_ORIGIN = true;
@@ -55,15 +57,17 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf
    private Throwable mInflateOrigin;
    private final SystemClock mSystemClock;
    private final RowInflaterTaskLogger mLogger;
    private final AsyncRowInflater mAsyncRowInflater;
    private long mInflateStartTimeMs;
    private UserTracker mUserTracker;

    @Inject
    public RowInflaterTask(SystemClock systemClock, RowInflaterTaskLogger logger,
            UserTracker userTracker) {
            UserTracker userTracker, AsyncRowInflater asyncRowInflater) {
        mSystemClock = systemClock;
        mLogger = logger;
        mUserTracker = userTracker;
        mAsyncRowInflater = asyncRowInflater;
    }

    /**
@@ -87,14 +91,20 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf
            mInflateOrigin = new Throwable("inflate requested here");
        }
        mListener = listener;
        AsyncLayoutInflater inflater = new AsyncLayoutInflater(context, makeRowInflater(entry));
        RowAsyncLayoutInflater asyncLayoutFactory = makeRowInflater(entry);
        mEntry = entry;
        entry.setInflationTask(this);

        mLogger.logInflateStart(entry);
        mInflateStartTimeMs = mSystemClock.elapsedRealtime();
        if (Flags.useNotifInflationThreadForRow()) {
            mAsyncRowInflater.inflate(context, asyncLayoutFactory,
                    R.layout.status_bar_notification_row, parent, this);
        } else {
            AsyncLayoutInflater inflater = new AsyncLayoutInflater(context, asyncLayoutFactory);
            inflater.inflate(R.layout.status_bar_notification_row, parent, listenerExecutor, this);
        }
    }

    /**
     * Inflates a new notificationView synchronously.
@@ -117,39 +127,6 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf
                entry, mSystemClock, mLogger, mUserTracker.getUserHandle());
    }

    /**
     * A {@link LayoutInflater} that is copy of BasicLayoutInflater.
     */
    private static class BasicRowInflater extends LayoutInflater {
        private static final String[] sClassPrefixList =
                {"android.widget.", "android.webkit.", "android.app."};
        BasicRowInflater(Context context) {
            super(context);
        }

        @Override
        public LayoutInflater cloneInContext(Context newContext) {
            return new BasicRowInflater(newContext);
        }

        @Override
        protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
            for (String prefix : sClassPrefixList) {
                try {
                    View view = createView(name, prefix, attrs);
                    if (view != null) {
                        return view;
                    }
                } catch (ClassNotFoundException e) {
                    // In this case we want to let the base class take a crack
                    // at it.
                }
            }

            return super.onCreateView(name, attrs);
        }
    }

    @VisibleForTesting
    public static class RowAsyncLayoutInflater implements AsyncLayoutFactory {
        private final NotificationEntry mEntry;
+2 −1
Original line number Diff line number Diff line
@@ -354,7 +354,8 @@ class ExpandableNotificationRowBuilder(
            RowInflaterTask(
                mFakeSystemClock,
                Mockito.mock(RowInflaterTaskLogger::class.java, STUB_ONLY),
                userTracker
                userTracker,
                Mockito.mock(AsyncRowInflater::class.java, STUB_ONLY),
            )
        val row = rowInflaterTask.inflateSynchronously(context, null, entry)