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

Commit f9c01162 authored by Ibrahim Yilmaz's avatar Ibrahim Yilmaz Committed by Android (Google) Code Review
Browse files

Merge "Set LayoutInflatorFactory to Notification RemoteViews" into udc-qpr-dev

parents dd8315d3 1a5c8337
Loading
Loading
Loading
Loading
+96 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.util.Log
import android.view.LayoutInflater
import android.view.View
import com.android.systemui.Dumpable
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.notification.row.NotificationRowModule.NOTIF_REMOTEVIEWS_FACTORIES
import com.android.systemui.util.asIndenting
import com.android.systemui.util.withIncreasedIndent
import java.io.PrintWriter
import javax.inject.Inject
import javax.inject.Named

/**
 * Implementation of [NotifLayoutInflaterFactory]. This class uses a set of
 * [NotifRemoteViewsFactory] objects to create replacement views for Notification RemoteViews.
 */
open class NotifLayoutInflaterFactory
@Inject
constructor(
    dumpManager: DumpManager,
    @Named(NOTIF_REMOTEVIEWS_FACTORIES)
    private val remoteViewsFactories: Set<@JvmSuppressWildcards NotifRemoteViewsFactory>
) : LayoutInflater.Factory2, Dumpable {
    init {
        dumpManager.registerNormalDumpable(TAG, this)
    }

    override fun onCreateView(
        parent: View?,
        name: String,
        context: Context,
        attrs: AttributeSet
    ): View? {
        var view: View? = null
        var handledFactory: NotifRemoteViewsFactory? = null
        for (layoutFactory in remoteViewsFactories) {
            view = layoutFactory.instantiate(parent, name, context, attrs)
            if (view != null) {
                check(handledFactory == null) {
                    "${layoutFactory.javaClass.name} tries to produce view. However, " +
                        "${handledFactory?.javaClass?.name} produced view for $name before."
                }
                handledFactory = layoutFactory
            }
        }
        logOnCreateView(name, view, handledFactory)
        return view
    }

    override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? =
        onCreateView(null, name, context, attrs)

    override fun dump(pw: PrintWriter, args: Array<out String>) {
        val indentingPW = pw.asIndenting()

        indentingPW.appendLine("$TAG ReplacementFactories:")
        indentingPW.withIncreasedIndent {
            remoteViewsFactories.forEach { indentingPW.appendLine(it.javaClass.simpleName) }
        }
    }

    private fun logOnCreateView(
        name: String,
        replacedView: View?,
        factory: NotifRemoteViewsFactory?
    ) {
        if (SPEW && replacedView != null && factory != null) {
            Log.d(TAG, "$factory produced view for $name: $replacedView")
        }
    }

    private companion object {
        private const val TAG = "NotifLayoutInflaterFac"
        private val SPEW = Log.isLoggable(TAG, Log.VERBOSE)
    }
}
+28 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.View

/** Interface used to create replacement view instances in Notification RemoteViews. */
interface NotifRemoteViewsFactory {

    /** return the replacement view instance for the given view name */
    fun instantiate(parent: View?, name: String, context: Context, attrs: AttributeSet): View?
}
+36 −11
Original line number Diff line number Diff line
@@ -79,6 +79,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
    private final ConversationNotificationProcessor mConversationProcessor;
    private final Executor mBgExecutor;
    private final SmartReplyStateInflater mSmartReplyStateInflater;
    private final NotifLayoutInflaterFactory mNotifLayoutInflaterFactory;

    @Inject
    NotificationContentInflater(
@@ -87,13 +88,15 @@ public class NotificationContentInflater implements NotificationRowContentBinder
            ConversationNotificationProcessor conversationProcessor,
            MediaFeatureFlag mediaFeatureFlag,
            @Background Executor bgExecutor,
            SmartReplyStateInflater smartRepliesInflater) {
            SmartReplyStateInflater smartRepliesInflater,
            NotifLayoutInflaterFactory notifLayoutInflaterFactory) {
        mRemoteViewCache = remoteViewCache;
        mRemoteInputManager = remoteInputManager;
        mConversationProcessor = conversationProcessor;
        mIsMediaInQS = mediaFeatureFlag.getEnabled();
        mBgExecutor = bgExecutor;
        mSmartReplyStateInflater = smartRepliesInflater;
        mNotifLayoutInflaterFactory = notifLayoutInflaterFactory;
    }

    @Override
@@ -137,7 +140,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder
                callback,
                mRemoteInputManager.getRemoteViewsOnClickHandler(),
                mIsMediaInQS,
                mSmartReplyStateInflater);
                mSmartReplyStateInflater,
                mNotifLayoutInflaterFactory);
        if (mInflateSynchronously) {
            task.onPostExecute(task.doInBackground());
        } else {
@@ -160,7 +164,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder
                bindParams.isLowPriority,
                bindParams.usesIncreasedHeight,
                bindParams.usesIncreasedHeadsUpHeight,
                packageContext);
                packageContext,
                mNotifLayoutInflaterFactory);

        result = inflateSmartReplyViews(result, reInflateFlags, entry,
                row.getContext(), packageContext,
@@ -298,7 +303,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder

    private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags,
            Notification.Builder builder, boolean isLowPriority, boolean usesIncreasedHeight,
            boolean usesIncreasedHeadsUpHeight, Context packageContext) {
            boolean usesIncreasedHeadsUpHeight, Context packageContext,
            NotifLayoutInflaterFactory notifLayoutInflaterFactory) {
        InflationProgress result = new InflationProgress();

        if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
@@ -316,7 +322,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
        if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
            result.newPublicView = builder.makePublicContentView(isLowPriority);
        }

        setNotifsViewsInflaterFactory(result, notifLayoutInflaterFactory);
        result.packageContext = packageContext;
        result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(false /* showingPublic */);
        result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText(
@@ -324,6 +330,22 @@ public class NotificationContentInflater implements NotificationRowContentBinder
        return result;
    }

    private static void setNotifsViewsInflaterFactory(InflationProgress result,
            NotifLayoutInflaterFactory notifLayoutInflaterFactory) {
        setRemoteViewsInflaterFactory(result.newContentView, notifLayoutInflaterFactory);
        setRemoteViewsInflaterFactory(result.newExpandedView,
                notifLayoutInflaterFactory);
        setRemoteViewsInflaterFactory(result.newHeadsUpView, notifLayoutInflaterFactory);
        setRemoteViewsInflaterFactory(result.newPublicView, notifLayoutInflaterFactory);
    }

    private static void setRemoteViewsInflaterFactory(RemoteViews remoteViews,
            NotifLayoutInflaterFactory notifLayoutInflaterFactory) {
        if (remoteViews != null) {
            remoteViews.setLayoutInflaterFactory(notifLayoutInflaterFactory);
        }
    }

    private static CancellationSignal apply(
            Executor bgExecutor,
            boolean inflateSynchronously,
@@ -348,7 +370,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder
                public void setResultView(View v) {
                    result.inflatedContentView = v;
                }

                @Override
                public RemoteViews getRemoteView() {
                    return result.newContentView;
@@ -800,6 +821,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
        private final ConversationNotificationProcessor mConversationProcessor;
        private final boolean mIsMediaInQS;
        private final SmartReplyStateInflater mSmartRepliesInflater;
        private final NotifLayoutInflaterFactory mNotifLayoutInflaterFactory;

        private AsyncInflationTask(
                Executor bgExecutor,
@@ -815,7 +837,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder
                InflationCallback callback,
                RemoteViews.InteractionHandler remoteViewClickHandler,
                boolean isMediaFlagEnabled,
                SmartReplyStateInflater smartRepliesInflater) {
                SmartReplyStateInflater smartRepliesInflater,
                NotifLayoutInflaterFactory notifLayoutInflaterFactory) {
            mEntry = entry;
            mRow = row;
            mBgExecutor = bgExecutor;
@@ -831,6 +854,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
            mCallback = callback;
            mConversationProcessor = conversationProcessor;
            mIsMediaInQS = isMediaFlagEnabled;
            mNotifLayoutInflaterFactory = notifLayoutInflaterFactory;
            entry.setInflationTask(this);
        }

@@ -874,7 +898,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder
                }
                InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
                        recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight,
                        mUsesIncreasedHeadsUpHeight, packageContext);
                        mUsesIncreasedHeadsUpHeight, packageContext,
                        mNotifLayoutInflaterFactory);
                InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState();
                InflationProgress result = inflateSmartReplyViews(
                        inflationProgress,
+22 −0
Original line number Diff line number Diff line
@@ -17,15 +17,26 @@
package com.android.systemui.statusbar.notification.row;

import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.flags.FeatureFlags;

import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.ElementsIntoSet;

import java.util.HashSet;
import java.util.Set;

import javax.inject.Named;

/**
 * Dagger Module containing notification row and view inflation implementations.
 */
@Module
public abstract class NotificationRowModule {
    public static final String NOTIF_REMOTEVIEWS_FACTORIES =
            "notif_remoteviews_factories";

    /**
     * Provides notification row content binder instance.
     */
@@ -41,4 +52,15 @@ public abstract class NotificationRowModule {
    @SysUISingleton
    public abstract NotifRemoteViewCache provideNotifRemoteViewCache(
            NotifRemoteViewCacheImpl cacheImpl);

    /** Provides view factories to be inflated in notification content. */
    @Provides
    @ElementsIntoSet
    @Named(NOTIF_REMOTEVIEWS_FACTORIES)
    static Set<NotifRemoteViewsFactory> provideNotifRemoteViewsFactories(
            FeatureFlags featureFlags
    ) {
        final Set<NotifRemoteViewsFactory> replacementFactories = new HashSet<>();
        return replacementFactories;
    }
}
+129 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertNotNull
import junit.framework.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations

/** Tests for [NotifLayoutInflaterFactory] */
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class NotifLayoutInflaterFactoryTest : SysuiTestCase() {

    @Mock private lateinit var attrs: AttributeSet

    @Before
    fun before() {
        MockitoAnnotations.initMocks(this)
    }

    @Test
    fun onCreateView_notMatchingViews_returnNull() {
        // GIVEN
        val layoutInflaterFactory =
            createNotifLayoutInflaterFactoryImpl(
                setOf(
                    createReplacementViewFactory("TextView") { context, attrs ->
                        FrameLayout(context)
                    }
                )
            )

        // WHEN
        val createView = layoutInflaterFactory.onCreateView("ImageView", mContext, attrs)

        // THEN
        assertNull(createView)
    }

    @Test
    fun onCreateView_matchingViews_returnReplacementView() {
        // GIVEN
        val layoutInflaterFactory =
            createNotifLayoutInflaterFactoryImpl(
                setOf(
                    createReplacementViewFactory("TextView") { context, attrs ->
                        FrameLayout(context)
                    }
                )
            )

        // WHEN
        val createView = layoutInflaterFactory.onCreateView("TextView", mContext, attrs)

        // THEN
        assertNotNull(createView)
        assertEquals(requireNotNull(createView)::class.java, FrameLayout::class.java)
    }

    @Test(expected = IllegalStateException::class)
    fun onCreateView_multipleFactory_throwIllegalStateException() {
        // GIVEN
        val layoutInflaterFactory =
            createNotifLayoutInflaterFactoryImpl(
                setOf(
                    createReplacementViewFactory("TextView") { context, attrs ->
                        FrameLayout(context)
                    },
                    createReplacementViewFactory("TextView") { context, attrs ->
                        LinearLayout(context)
                    }
                )
            )

        // WHEN
        layoutInflaterFactory.onCreateView("TextView", mContext, attrs)
    }

    private fun createNotifLayoutInflaterFactoryImpl(
        replacementViewFactories: Set<@JvmSuppressWildcards NotifRemoteViewsFactory>
    ) = NotifLayoutInflaterFactory(DumpManager(), replacementViewFactories)

    private fun createReplacementViewFactory(
        replacementName: String,
        createView: (context: Context, attrs: AttributeSet) -> View
    ) =
        object : NotifRemoteViewsFactory {
            override fun instantiate(
                parent: View?,
                name: String,
                context: Context,
                attrs: AttributeSet
            ): View? =
                if (replacementName == name) {
                    createView(context, attrs)
                } else {
                    null
                }
        }
}
Loading