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

Commit 07fa60db authored by Daniel Akinola's avatar Daniel Akinola Committed by Android (Google) Code Review
Browse files

Merge "Linter for shade dialog creation" into main

parents 4e42aebc fda55e5c
Loading
Loading
Loading
Loading
+14 −12
Original line number Diff line number Diff line
@@ -29,6 +29,14 @@ import com.intellij.psi.PsiParameter
import org.jetbrains.uast.UClass
import org.jetbrains.uast.getContainingUFile

/**
 * Lint check to ensure that when including a Context or Context-dependent argument in
 * shade-relevant packages, the argument has the @ShadeDisplayAware annotation.
 *
 * This is to ensure that Context-dependent components correctly handle Configuration changes when
 * the shade is moved to a different display. @ShadeDisplayAware-annotated components will update
 * accordingly to reflect the new display.
 */
class ShadeDisplayAwareDetector : Detector(), SourceCodeScanner {
    override fun getApplicableUastTypes() = listOf(UClass::class.java)

@@ -38,8 +46,8 @@ class ShadeDisplayAwareDetector : Detector(), SourceCodeScanner {
                for (constructor in node.constructors) {
                    // Visit all injected constructors in shade-relevant packages
                    if (!constructor.hasAnnotation(INJECT_ANNOTATION)) continue
                    if (!isInRelevantShadePackage(node)) continue
                    if (IGNORED_PACKAGES.contains(node.qualifiedName)) continue
                    if (!isInRelevantShadePackage(node.getContainingUFile()?.packageName)) continue
                    if (IGNORED_CLASSES.contains(node.qualifiedName)) continue

                    for (parameter in constructor.parameterList.parameters) {
                        if (parameter.shouldReport()) {
@@ -84,24 +92,19 @@ class ShadeDisplayAwareDetector : Detector(), SourceCodeScanner {
                CONFIG_INTERACTOR,
            )

        private val CONFIG_CLASSES = setOf(CONFIG_STATE, CONFIG_CONTROLLER, CONFIG_INTERACTOR)

        private val SHADE_WINDOW_PACKAGES =
            listOf(
                "com.android.systemui.biometrics",
                "com.android.systemui.bouncer",
                "com.android.systemui.keyboard.docking.ui.viewmodel",
                "com.android.systemui.media.controls.ui.controller",
                "com.android.systemui.qs",
                "com.android.systemui.shade",
                "com.android.systemui.statusbar.notification",
                "com.android.systemui.statusbar.lockscreen",
                "com.android.systemui.unfold.domain.interactor",
            )

        private val IGNORED_PACKAGES =
            setOf(
                "com.android.systemui.biometrics.UdfpsController",
                "com.android.systemui.qs.customize.TileAdapter",
            )
        private val IGNORED_CLASSES = setOf("com.android.systemui.statusbar.phone.SystemUIDialog")

        private fun PsiParameter.shouldReport(): Boolean {
            val className = type.canonicalText
@@ -116,8 +119,7 @@ class ShadeDisplayAwareDetector : Detector(), SourceCodeScanner {
            return true
        }

        private fun isInRelevantShadePackage(node: UClass): Boolean {
            val packageName = node.getContainingUFile()?.packageName
        fun isInRelevantShadePackage(packageName: String?): Boolean {
            if (packageName.isNullOrBlank()) return false
            return SHADE_WINDOW_PACKAGES.any { relevantPackage ->
                packageName.startsWith(relevantPackage)
+124 −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.internal.systemui.lint

import com.android.internal.systemui.lint.ShadeDisplayAwareDetector.Companion.isInRelevantShadePackage
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.android.tools.lint.detector.api.UastLintUtils.Companion.tryResolveUDeclaration
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.getContainingUClass
import org.jetbrains.uast.getContainingUFile

/**
 * Lint check to ensure that when creating dialogs shade-relevant packages, the correct Context is
 * provided.
 *
 * This is to ensure that the dialog is created with the correct context when the shade is moved to
 * a different display. When the shade is moved, the configuration might change, and only
 * `@ShadeDisplayAware`-annotated components will update accordingly to reflect the new display.
 *
 * Example:
 * ```kotlin
 * class ExampleClass
 *      @Inject
 *      constructor(private val contextInteractor: ShadeDialogContextInteractor) {
 *
 *      fun showDialog() {
 *          val dialog = systemUIDialogFactory.create(delegate, contextInteractor.context)
 *          dialog.show()
 *      }
 *  }
 * ```
 */
// TODO: b/396066687 - update linter after refactoring to use ShadeDialogFactory
class ShadeDisplayAwareDialogDetector : Detector(), SourceCodeScanner {
    override fun getApplicableMethodNames(): List<String> = listOf(CREATE_METHOD)

    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
        if (!isInRelevantShadePackage(node.getContainingUFile()?.packageName)) return
        if (!context.evaluator.isMemberInClass(method, SYSUI_DIALOG_FACTORY)) return
        val contextArg =
            node.valueArguments.find {
                it.getExpressionType()?.canonicalText == "android.content.Context"
            }
        if (contextArg == null) {
            context.report(
                issue = ISSUE,
                scope = node,
                location = context.getNameLocation(node),
                message =
                    "SystemUIDialog.Factory#create requires a Context that accounts for the " +
                        "shade's display. Use create(shadeDialogContextInteractor.getContext()) " +
                        "or create(shadeDialogContextInteractor.context) to provide the correct Context.",
            )
        } else {
            val isProvidedByContextInteractor =
                contextArg.tryResolveUDeclaration()?.getContainingUClass()?.qualifiedName ==
                    SHADE_DIALOG_CONTEXT_INTERACTOR

            if (!isProvidedByContextInteractor) {
                context.report(
                    issue = ISSUE,
                    scope = contextArg,
                    location = context.getNameLocation(contextArg),
                    message =
                        "In shade-relevant packages, SystemUIDialog.Factory#create must be called " +
                            "with the Context directly from ShadeDialogContextInteractor " +
                            "(ShadeDialogContextInteractor.context or getContext()). " +
                            "Avoid intermediate variables or function calls. This direct usage " +
                            "is required to ensure proper shade display handling.",
                )
            }
        }
    }

    companion object {
        private const val CREATE_METHOD = "create"
        private const val SYSUI_DIALOG_FACTORY =
            "com.android.systemui.statusbar.phone.SystemUIDialog.Factory"
        private const val SHADE_DIALOG_CONTEXT_INTERACTOR =
            "com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor"

        @JvmField
        val ISSUE: Issue =
            Issue.create(
                id = "ShadeDisplayAwareDialogChecker",
                briefDescription = "Checking for shade display aware context when creating dialogs",
                explanation =
                    """
                Dialogs created by the notification shade must use a special Context to appear
                on the correct display, especially when the shade is not on the default display.
            """
                        .trimIndent(),
                category = Category.CORRECTNESS,
                priority = 8,
                severity = Severity.WARNING,
                implementation =
                    Implementation(
                        ShadeDisplayAwareDialogDetector::class.java,
                        Scope.JAVA_FILE_SCOPE,
                    ),
            )
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ class SystemUIIssueRegistry : IssueRegistry() {
                TestFunctionNameViolationDetector.ISSUE,
                MissingApacheLicenseDetector.ISSUE,
                ShadeDisplayAwareDetector.ISSUE,
                ShadeDisplayAwareDialogDetector.ISSUE,
                RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING,
                RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR,
            )
+0 −28
Original line number Diff line number Diff line
@@ -410,34 +410,6 @@ class ShadeDisplayAwareDetectorTest : SystemUILintDetectorTest() {
            .expectClean()
    }

    @Test
    fun injectedConstructor_inExemptPackage_withRelevantParameter_withoutAnnotation() {
        lint()
            .files(
                TestFiles.java(
                    """
                        package com.android.systemui.qs.customize;

                        import javax.inject.Inject;
                        import com.android.systemui.qs.dagger.QSThemedContext;
                        import android.content.Context;

                        public class TileAdapter {
                            @Inject
                            public TileAdapter(@QSThemedContext Context context) {}
                        }
                    """
                        .trimIndent()
                ),
                *androidStubs,
                *otherStubs,
            )
            .issues(ShadeDisplayAwareDetector.ISSUE)
            .testModes(TestMode.DEFAULT)
            .run()
            .expectClean()
    }

    private fun errorMsgString(lineNumber: Int, className: String) =
        "src/com/android/systemui/shade/example/ExampleClass.kt:$lineNumber: Error: UI elements of " +
            "the shade window should use ShadeDisplayAware-annotated $className, as the shade " +
+541 −0

File added.

Preview size limit exceeded, changes collapsed.