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

Commit 18f19587 authored by Devarshi Bhatt's avatar Devarshi Bhatt
Browse files

Add linter to restrict direct usage of registerContentObserver() API.

Forbid direct usage of register/unregister content observer APIs from the ContentResolver class.
Instead direct the caller to use wrapper APIs in SettingsProxy and its sub-classes.

Bug: 353191254
Test: atest RegisterContentObserverViaContentResolverDetectorTest
Flag: NONE new linter check

Change-Id: I50db306d5dc7c2ac6a146586f7d336c32765b462
parent 9a72f7a6
Loading
Loading
Loading
Loading
+105 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.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.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.getParentOfType

/**
 * Checks if registerContentObserver/registerContentObserverAsUser/unregisterContentObserver is
 * called on a ContentResolver (or subclasses), and directs the caller to using
 * com.android.systemui.util.settings.SettingsProxy or its sub-classes.
 */
@Suppress("UnstableApiUsage")
class RegisterContentObserverViaContentResolverDetector : Detector(), SourceCodeScanner {

    override fun getApplicableMethodNames(): List<String> {
        return CONTENT_RESOLVER_METHOD_LIST
    }

    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
        val classQualifiedName = node.getParentOfType(UClass::class.java)?.qualifiedName
        if (classQualifiedName in CLASSNAME_ALLOWLIST) {
            // Don't warn for class we want the developers to use.
            return
        }

        val evaluator = context.evaluator
        if (evaluator.isMemberInSubClassOf(method, "android.content.ContentResolver")) {
            context.report(
                issue = CONTENT_RESOLVER_ERROR,
                location = context.getNameLocation(node),
                message =
                    "`ContentResolver.${method.name}()` should be replaced with " +
                        "an appropriate interface API call, for eg. " +
                        "`<SettingsProxy>/<UserSettingsProxy>.${method.name}()`"
            )
        }
    }

    companion object {
        @JvmField
        val CONTENT_RESOLVER_ERROR: Issue =
            Issue.create(
                id = "RegisterContentObserverViaContentResolver",
                briefDescription =
                    "Content observer registration done via `ContentResolver`" +
                        "instead of `SettingsProxy or child interfaces.`",
                // lint trims indents and converts \ to line continuations
                explanation =
                    """
                        Use registerContentObserver/unregisterContentObserver methods in \
                        `SettingsProxy`, `UserSettingsProxy` or `GlobalSettings` class instead of \
                        using `ContentResolver.registerContentObserver` or \
                        `ContentResolver.unregisterContentObserver`.""",
                category = Category.PERFORMANCE,
                priority = 10,
                severity = Severity.ERROR,
                implementation =
                    Implementation(
                        RegisterContentObserverViaContentResolverDetector::class.java,
                        Scope.JAVA_FILE_SCOPE
                    )
            )

        private val CLASSNAME_ALLOWLIST =
            listOf(
                "com.android.systemui.util.settings.SettingsProxy",
                "com.android.systemui.util.settings.UserSettingsProxy",
                "com.android.systemui.util.settings.GlobalSettings",
                "com.android.systemui.util.settings.SecureSettings",
                "com.android.systemui.util.settings.SystemSettings"
            )

        private val CONTENT_RESOLVER_METHOD_LIST =
            listOf(
                "registerContentObserver",
                "registerContentObserverAsUser",
                "unregisterContentObserver"
            )
    }
}
+2 −1
Original line number Diff line number Diff line
@@ -46,7 +46,8 @@ class SystemUIIssueRegistry : IssueRegistry() {
                DemotingTestWithoutBugDetector.ISSUE,
                TestFunctionNameViolationDetector.ISSUE,
                MissingApacheLicenseDetector.ISSUE,
                RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING
                RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING,
                RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR
            )

    override val api: Int
+207 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.tools.lint.checks.infrastructure.TestFiles
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test

class RegisterContentObserverViaContentResolverDetectorTest : SystemUILintDetectorTest() {

    override fun getDetector(): Detector = RegisterContentObserverViaContentResolverDetector()

    override fun getIssues(): List<Issue> =
        listOf(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)

    @Test
    fun testRegisterContentObserver_throwError() {
        lint()
            .files(
                TestFiles.java(
                        """
                    package test.pkg;
                    import android.content.Context;

                    public class TestClass {
                        public void register(Context context) {
                          context.getContentResolver().
                            registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
                                false, mSettingObserver);
                        }
                    }
                """
                    )
                    .indented(),
                *androidStubs
            )
            .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
            .run()
            .expect(
                """
                src/test/pkg/TestClass.java:7: Error: ContentResolver.registerContentObserver() should be replaced with an appropriate interface API call, for eg. <SettingsProxy>/<UserSettingsProxy>.registerContentObserver() [RegisterContentObserverViaContentResolver]
                        registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
                        ~~~~~~~~~~~~~~~~~~~~~~~
                1 errors, 0 warnings
            """
                    .trimIndent()
            )
    }

    @Test
    fun testRegisterContentObserverForUser_throwError() {
        lint()
            .files(
                TestFiles.java(
                        """
                    package test.pkg;
                    import android.content.Context;

                    public class TestClass {
                        public void register(Context context) {
                          context.getContentResolver().
                            registerContentObserverAsUser(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
                                false, mSettingObserver);
                        }
                    }
                """
                    )
                    .indented(),
                *androidStubs
            )
            .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
            .run()
            .expect(
                """
                src/test/pkg/TestClass.java:7: Error: ContentResolver.registerContentObserverAsUser() should be replaced with an appropriate interface API call, for eg. <SettingsProxy>/<UserSettingsProxy>.registerContentObserverAsUser() [RegisterContentObserverViaContentResolver]
        registerContentObserverAsUser(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
            """
                    .trimIndent()
            )
    }

    @Test
    fun testSuppressRegisterContentObserver() {
        lint()
            .files(
                TestFiles.java(
                        """
                    package test.pkg;
                    import android.content.Context;

                    public class TestClass {
                        @SuppressWarnings("RegisterContentObserverViaContentResolver")
                        public void register(Context context) {
                          context.getContentResolver().
                            registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
                                false, mSettingObserver);
                        }
                    }
                """
                    )
                    .indented(),
                *androidStubs
            )
            .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
            .run()
            .expectClean()
    }

    @Test
    fun testRegisterContentObserverInSettingsProxy_allowed() {
        lint()
            .files(
                TestFiles.java(
                        """
                    package com.android.systemui.util.settings;
                    import android.content.Context;

                    public class SettingsProxy {
                        public void register(Context context) {
                          context.getContentResolver().
                            registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
                                false, mSettingObserver);
                        }
                    }
                """
                    )
                    .indented(),
                *androidStubs
            )
            .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
            .run()
            .expectClean()
    }

    @Test
    fun testNoopIfNoCall() {
        lint()
            .files(
                TestFiles.java(
                        """
                    package test.pkg;
                    import android.content.Context;

                    public class SettingsProxy {
                        public void register(Context context) {
                        }
                    }
                """
                    )
                    .indented(),
                *androidStubs
            )
            .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
            .run()
            .expectClean()
    }

    @Test
    fun testUnRegisterContentObserver_throwError() {
        lint()
            .files(
                TestFiles.java(
                        """
                    package test.pkg;
                    import android.content.Context;

                    public class TestClass {
                        public void register(Context context) {
                          context.getContentResolver().
                            unregisterContentObserver(mSettingObserver);
                        }
                    }
                """
                    )
                    .indented(),
                *androidStubs
            )
            .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
            .run()
            .expect(
                """
                src/test/pkg/TestClass.java:7: Error: ContentResolver.unregisterContentObserver() should be replaced with an appropriate interface API call, for eg. <SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver() [RegisterContentObserverViaContentResolver]
                        unregisterContentObserver(mSettingObserver);
                        ~~~~~~~~~~~~~~~~~~~~~~~~~
                1 errors, 0 warnings
            """
                    .trimIndent()
            )
    }
}
+627 −0

File changed.

Preview size limit exceeded, changes collapsed.