[flutter_plugin_tools] Add a command to lint Android code (#4206)

Adds a new `lint-android` command to run `gradlew lint` on Android plugins.

Also standardizes the names of the Cirrus tasks that run all the build and platform-specific (i.e., not Dart unit test) tests for each platform, as they were getting unnecessarily long and complex in some cases.

Fixes https://github.com/flutter/flutter/issues/87071
diff --git a/.cirrus.yml b/.cirrus.yml
index ffdd71d..d830a2a 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -119,7 +119,7 @@
       setup_script:
         - flutter config --enable-linux-desktop
       << : *BUILD_ALL_PLUGINS_APP_TEMPLATE
-    - name: build-linux+drive-examples
+    - name: linux-build+platform-tests
       env:
         matrix:
           CHANNEL: "master"
@@ -146,7 +146,7 @@
     memory: 12G
   matrix:
     ### Android tasks ###
-    - name: build-apks+android-unit+firebase-test-lab
+    - name: android-build+platform-tests
       env:
         matrix:
           PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4"
@@ -165,6 +165,13 @@
         - export CIRRUS_CHANGE_MESSAGE=""
         - export CIRRUS_COMMIT_MESSAGE=""
         - ./script/tool_runner.sh build-examples --apk
+      lint_script:
+        # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they
+        # might include non-ASCII characters which makes Gradle crash.
+        # TODO(stuartmorgan): See https://github.com/flutter/flutter/issues/24935
+        - export CIRRUS_CHANGE_MESSAGE=""
+        - export CIRRUS_COMMIT_MESSAGE=""
+        - ./script/tool_runner.sh lint-android # must come after build-examples
       native_unit_test_script:
         # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they
         # might include non-ASCII characters which makes Gradle crash.
@@ -186,8 +193,14 @@
         - else
         -   echo "This user does not have permission to run Firebase Test Lab tests."
         - fi
+      # Upload the full lint results to Cirrus to display in the results UI.
+      always:
+        android-lint_artifacts:
+          path: "**/reports/lint-results-debug.xml"
+          type: text/xml
+          format: android-lint
     ### Web tasks ###
-    - name: build-web+drive-examples
+    - name: web-build+platform-tests
       env:
         matrix:
           CHANNEL: "master"
@@ -220,7 +233,7 @@
           CHANNEL: "master"
           CHANNEL: "stable"
       << : *BUILD_ALL_PLUGINS_APP_TEMPLATE
-    - name: build-ipas+drive-examples
+    - name: ios-build+platform-tests
       env:
         PATH: $PATH:/usr/local/bin
         matrix:
@@ -256,7 +269,7 @@
       setup_script:
         - flutter config --enable-macos-desktop
       << : *BUILD_ALL_PLUGINS_APP_TEMPLATE
-    - name: build-macos+drive-examples
+    - name: macos-build+platform-tests
       env:
         matrix:
           CHANNEL: "master"
diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md
index 71f47ce..d53b932 100644
--- a/packages/android_alarm_manager/CHANGELOG.md
+++ b/packages/android_alarm_manager/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## NEXT
 
 * Remove support for the V1 Android embedding.
+* Updated Android lint settings.
 
 ## 2.0.2
 
diff --git a/packages/android_alarm_manager/android/build.gradle b/packages/android_alarm_manager/android/build.gradle
index be74109..b173137 100644
--- a/packages/android_alarm_manager/android/build.gradle
+++ b/packages/android_alarm_manager/android/build.gradle
@@ -38,6 +38,8 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
+        baseline file("lint-baseline.xml")
     }
 
 
diff --git a/packages/android_alarm_manager/android/lint-baseline.xml b/packages/android_alarm_manager/android/lint-baseline.xml
new file mode 100644
index 0000000..de58861
--- /dev/null
+++ b/packages/android_alarm_manager/android/lint-baseline.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 3.3.0" client="gradle" variant="debug" version="3.3.0">
+
+    <issue
+        id="LongLogTag"
+        message="The logging tag can be at most 23 characters, was 25 (AndroidAlarmManagerPlugin)"
+        errorLine1="      Log.i(TAG, &quot;onAttachedToEngine&quot;);"
+        errorLine2="            ~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java"
+            line="75"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="LongLogTag"
+        message="The logging tag can be at most 23 characters, was 25 (AndroidAlarmManagerPlugin)"
+        errorLine1="    Log.i(TAG, &quot;onDetachedFromEngine&quot;);"
+        errorLine2="          ~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java"
+            line="96"
+            column="11"/>
+    </issue>
+
+    <issue
+        id="LongLogTag"
+        message="The logging tag can be at most 23 characters, was 25 (FlutterBackgroundExecutor)"
+        errorLine1="      Log.e(TAG, &quot;Background isolate already started&quot;);"
+        errorLine2="            ~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/androidalarmmanager/FlutterBackgroundExecutor.java"
+            line="130"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="LongLogTag"
+        message="The logging tag can be at most 23 characters, was 25 (FlutterBackgroundExecutor)"
+        errorLine1="    Log.i(TAG, &quot;Starting AlarmService...&quot;);"
+        errorLine2="          ~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/androidalarmmanager/FlutterBackgroundExecutor.java"
+            line="134"
+            column="11"/>
+    </issue>
+
+    <issue
+        id="StaticFieldLeak"
+        message="Do not place Android context classes in static fields (static reference to `AndroidAlarmManagerPlugin` which has field `context` pointing to `Context`); this is a memory leak (and also breaks Instant Run)"
+        errorLine1="  private static AndroidAlarmManagerPlugin instance;"
+        errorLine2="          ~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java"
+            line="43"
+            column="11"/>
+    </issue>
+
+</issues>
diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md
index 71428c5..82cd5db 100644
--- a/packages/android_intent/CHANGELOG.md
+++ b/packages/android_intent/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## NEXT
 
 * Remove references to the V1 Android embedding.
+* Updated Android lint settings.
 
 ## 2.0.2
 
diff --git a/packages/android_intent/android/build.gradle b/packages/android_intent/android/build.gradle
index b0238b7..e8b9f38 100644
--- a/packages/android_intent/android/build.gradle
+++ b/packages/android_intent/android/build.gradle
@@ -35,6 +35,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
 
 
diff --git a/packages/battery/battery/CHANGELOG.md b/packages/battery/battery/CHANGELOG.md
index 8590e64..ddc912d 100644
--- a/packages/battery/battery/CHANGELOG.md
+++ b/packages/battery/battery/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## NEXT
 
 * Remove references to the Android v1 embedding.
+* Updated Android lint settings.
 
 ## 2.0.3
 
diff --git a/packages/battery/battery/android/build.gradle b/packages/battery/battery/android/build.gradle
index 1e48489..14f5038 100644
--- a/packages/battery/battery/android/build.gradle
+++ b/packages/battery/battery/android/build.gradle
@@ -30,6 +30,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
 
 
diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index d455ddb..6948980 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,3 +1,7 @@
+## NEXT
+
+* Updated Android lint settings.
+
 ## 0.8.1+7
 
 * Fix device orientation sometimes not affecting the camera preview orientation.
diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle
index 6ceed97..9bbafb6 100644
--- a/packages/camera/camera/android/build.gradle
+++ b/packages/camera/camera/android/build.gradle
@@ -35,6 +35,8 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
+        baseline file("lint-baseline.xml")
     }
     compileOptions {
         sourceCompatibility = '1.8'
diff --git a/packages/camera/camera/android/lint-baseline.xml b/packages/camera/camera/android/lint-baseline.xml
new file mode 100644
index 0000000..4ddaafa
--- /dev/null
+++ b/packages/camera/camera/android/lint-baseline.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 3.5.0" client="gradle" variant="debug" version="3.5.0">
+
+    <issue
+        id="Assert"
+        message="Assertions are unreliable in Dalvik and unimplemented in ART. Use `BuildConfig.DEBUG` conditional checks instead."
+        errorLine1="    assert (boundaries.getWidth() > 0 &amp;&amp; boundaries.getHeight() > 0);"
+        errorLine2="    ~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java"
+            line="73"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="Assert"
+        message="Assertions are unreliable in Dalvik and unimplemented in ART. Use `BuildConfig.DEBUG` conditional checks instead."
+        errorLine1="    assert (x >= 0 &amp;&amp; x &lt;= 1);"
+        errorLine2="    ~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java"
+            line="74"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="Assert"
+        message="Assertions are unreliable in Dalvik and unimplemented in ART. Use `BuildConfig.DEBUG` conditional checks instead."
+        errorLine1="    assert (y >= 0 &amp;&amp; y &lt;= 1);"
+        errorLine2="    ~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java"
+            line="75"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="Assert"
+        message="Assertions are unreliable in Dalvik and unimplemented in ART. Use `BuildConfig.DEBUG` conditional checks instead."
+        errorLine1="    assert (maxBoundaries == null || maxBoundaries.getWidth() > 0);"
+        errorLine2="    ~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/camera/CameraRegions.java"
+            line="16"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="Assert"
+        message="Assertions are unreliable in Dalvik and unimplemented in ART. Use `BuildConfig.DEBUG` conditional checks instead."
+        errorLine1="    assert (maxBoundaries == null || maxBoundaries.getHeight() > 0);"
+        errorLine2="    ~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/camera/CameraRegions.java"
+            line="17"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="Assert"
+        message="Assertions are unreliable in Dalvik and unimplemented in ART. Use `BuildConfig.DEBUG` conditional checks instead."
+        errorLine1="    assert (x >= 0 &amp;&amp; x &lt;= 1);"
+        errorLine2="    ~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/camera/CameraRegions.java"
+            line="50"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="Assert"
+        message="Assertions are unreliable in Dalvik and unimplemented in ART. Use `BuildConfig.DEBUG` conditional checks instead."
+        errorLine1="    assert (y >= 0 &amp;&amp; y &lt;= 1);"
+        errorLine2="    ~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/camera/CameraRegions.java"
+            line="51"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="SwitchIntDef"
+        message="Switch statement on an `int` with known associated constant missing case `Configuration.ORIENTATION_SQUARE`, `Configuration.ORIENTATION_UNDEFINED`"
+        errorLine1="    switch (orientation) {"
+        errorLine2="    ~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java"
+            line="143"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="SwitchIntDef"
+        message="Switch statement on an `int` with known associated constant missing case `Configuration.ORIENTATION_SQUARE`, `Configuration.ORIENTATION_UNDEFINED`"
+        errorLine1="    switch (orientation) {"
+        errorLine2="    ~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java"
+            line="264"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="ObsoleteSdkInt"
+        message="Unnecessary; SDK_INT is never &lt; 21"
+        errorLine1="    if (Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.LOLLIPOP) {"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/camera/CameraPlugin.java"
+            line="102"
+            column="9"/>
+    </issue>
+
+</issues>
diff --git a/packages/connectivity/connectivity/CHANGELOG.md b/packages/connectivity/connectivity/CHANGELOG.md
index 5804748..f548969 100644
--- a/packages/connectivity/connectivity/CHANGELOG.md
+++ b/packages/connectivity/connectivity/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## NEXT
 
 * Remove references to the Android V1 embedding.
+* Updated Android lint settings.
 
 ## 3.0.6
 
diff --git a/packages/connectivity/connectivity/android/build.gradle b/packages/connectivity/connectivity/android/build.gradle
index 53a390b..983f29b 100644
--- a/packages/connectivity/connectivity/android/build.gradle
+++ b/packages/connectivity/connectivity/android/build.gradle
@@ -35,6 +35,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
 
 
diff --git a/packages/device_info/device_info/CHANGELOG.md b/packages/device_info/device_info/CHANGELOG.md
index 669423c..97349d4 100644
--- a/packages/device_info/device_info/CHANGELOG.md
+++ b/packages/device_info/device_info/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## NEXT
 
 * Remove references to the Android V1 embedding.
+* Updated Android lint settings.
 
 ## 2.0.2
 
diff --git a/packages/device_info/device_info/android/build.gradle b/packages/device_info/device_info/android/build.gradle
index 51ec2a7..ed89da4 100644
--- a/packages/device_info/device_info/android/build.gradle
+++ b/packages/device_info/device_info/android/build.gradle
@@ -30,6 +30,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
 
 
diff --git a/packages/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md
index 10e5ae5..e00ea70 100644
--- a/packages/espresso/CHANGELOG.md
+++ b/packages/espresso/CHANGELOG.md
@@ -1,3 +1,7 @@
+## NEXT
+
+* Updated Android lint settings.
+
 ## 0.1.0+3
 
 * Remove references to the Android v1 embedding.
diff --git a/packages/espresso/android/build.gradle b/packages/espresso/android/build.gradle
index 8cd5481..da0cd2e 100644
--- a/packages/espresso/android/build.gradle
+++ b/packages/espresso/android/build.gradle
@@ -30,6 +30,8 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
+        baseline file("lint-baseline.xml")
     }
 
 
diff --git a/packages/espresso/android/lint-baseline.xml b/packages/espresso/android/lint-baseline.xml
new file mode 100644
index 0000000..19b349f
--- /dev/null
+++ b/packages/espresso/android/lint-baseline.xml
@@ -0,0 +1,389 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 3.5.0" client="gradle" variant="debug" version="3.5.0">
+
+    <issue
+        id="DefaultLocale"
+        message="Implicitly using the default locale is a common source of bugs: Use `String.format(Locale, ...)` instead"
+        errorLine1="                String.format("
+        errorLine2="                ^">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java"
+            line="180"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="DefaultLocale"
+        message="Implicitly using the default locale is a common source of bugs: Use `String.format(Locale, ...)` instead"
+        errorLine1="                String.format("
+        errorLine2="                ^">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java"
+            line="186"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="DefaultLocale"
+        message="Implicitly using the default locale is a common source of bugs: Use `String.format(Locale, ...)` instead"
+        errorLine1="                String.format("
+        errorLine2="                ^">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java"
+            line="192"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="          &amp;&amp; Objects.equals(errorObject.message, this.message)"
+        errorLine2="                     ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/ErrorObject.java"
+            line="50"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="          &amp;&amp; Objects.equals(errorObject.data, this.data);"
+        errorLine2="                     ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/ErrorObject.java"
+            line="51"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#hashCode`"
+        errorLine1="    hash = hash * 31 + Objects.hashCode(message);"
+        errorLine2="                               ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/ErrorObject.java"
+            line="60"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#hashCode`"
+        errorLine1="    hash = hash * 31 + Objects.hashCode(data);"
+        errorLine2="                               ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/ErrorObject.java"
+            line="61"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="        return Objects.equals(isolate.id, this.id)"
+        errorLine2="                       ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetVmResponse.java"
+            line="114"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="            &amp;&amp; Objects.equals(isolate.runnable, this.runnable)"
+        errorLine2="                       ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetVmResponse.java"
+            line="115"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="            &amp;&amp; Objects.equals(isolate.extensionRpcList, this.extensionRpcList);"
+        errorLine2="                       ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetVmResponse.java"
+            line="116"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#hash`"
+        errorLine1="      return Objects.hash(id, runnable, extensionRpcList);"
+        errorLine2="                     ~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetVmResponse.java"
+            line="124"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="        return Objects.equals(this.name, widgetProperty.name)"
+        errorLine2="                       ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsResponse.java"
+            line="178"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="            &amp;&amp; Objects.equals(this.value, widgetProperty.value)"
+        errorLine2="                       ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsResponse.java"
+            line="179"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="            &amp;&amp; Objects.equals(this.description, widgetProperty.description);"
+        errorLine2="                       ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsResponse.java"
+            line="180"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#hash`"
+        errorLine1="      return Objects.hash(name, value, description);"
+        errorLine2="                     ~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsResponse.java"
+            line="186"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="      return Objects.equals(objRequest.id, this.id)"
+        errorLine2="                     ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcRequest.java"
+            line="136"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="          &amp;&amp; Objects.equals(objRequest.method, this.method)"
+        errorLine2="                     ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcRequest.java"
+            line="137"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="          &amp;&amp; Objects.equals(objRequest.params, this.params);"
+        errorLine2="                     ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcRequest.java"
+            line="138"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#hashCode`"
+        errorLine1="    int hash = Objects.hashCode(id);"
+        errorLine2="                       ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcRequest.java"
+            line="146"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#hashCode`"
+        errorLine1="    hash = hash * 31 + Objects.hashCode(method);"
+        errorLine2="                               ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcRequest.java"
+            line="147"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#hashCode`"
+        errorLine1="    hash = hash * 31 + Objects.hashCode(params);"
+        errorLine2="                               ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcRequest.java"
+            line="148"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="      return Objects.equals(objResponse.id, this.id)"
+        errorLine2="                     ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcResponse.java"
+            line="141"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="          &amp;&amp; Objects.equals(objResponse.result, this.result)"
+        errorLine2="                     ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcResponse.java"
+            line="142"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="          &amp;&amp; Objects.equals(objResponse.error, this.error);"
+        errorLine2="                     ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcResponse.java"
+            line="143"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#hashCode`"
+        errorLine1="    int hash = Objects.hashCode(id);"
+        errorLine2="                       ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcResponse.java"
+            line="151"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#hashCode`"
+        errorLine1="    hash = hash * 31 + Objects.hashCode(result);"
+        errorLine2="                               ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcResponse.java"
+            line="152"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#hashCode`"
+        errorLine1="    hash = hash * 31 + Objects.hashCode(error);"
+        errorLine2="                               ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcResponse.java"
+            line="153"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="      return Objects.equals(actionId, otherAction.actionId);"
+        errorLine2="                     ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/api/SyntheticAction.java"
+            line="56"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#hashCode`"
+        errorLine1="    return Objects.hashCode(actionId);"
+        errorLine2="                   ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/api/SyntheticAction.java"
+            line="64"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="      return Objects.equals(widget.valueKey, this.valueKey)"
+        errorLine2="                     ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/model/WidgetInfo.java"
+            line="78"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="          &amp;&amp; Objects.equals(widget.runtimeType, this.runtimeType)"
+        errorLine2="                     ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/model/WidgetInfo.java"
+            line="79"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="          &amp;&amp; Objects.equals(widget.text, this.text)"
+        errorLine2="                     ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/model/WidgetInfo.java"
+            line="80"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#equals`"
+        errorLine1="          &amp;&amp; Objects.equals(widget.tooltip, this.tooltip);"
+        errorLine2="                     ~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/model/WidgetInfo.java"
+            line="81"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 19 (current min is 16): `java.util.Objects#hash`"
+        errorLine1="    return Objects.hash(valueKey, runtimeType, text, tooltip);"
+        errorLine2="                   ~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/model/WidgetInfo.java"
+            line="89"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="VisibleForTests"
+        message="This method should only be accessed from tests or within private scope"
+        errorLine1="          ((io.flutter.embedding.android.FlutterView) flutterView).getAttachedFlutterEngine();"
+        errorLine2="                                                                   ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmServiceUtil.java"
+            line="83"
+            column="68"/>
+    </issue>
+
+</issues>
diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md
index 6a05ed0..7e567d8 100644
--- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md
+++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md
@@ -1,3 +1,7 @@
+## NEXT
+
+* Updated Android lint settings.
+
 ## 2.0.3
 
 * Remove references to the Android V1 embedding.
diff --git a/packages/flutter_plugin_android_lifecycle/android/build.gradle b/packages/flutter_plugin_android_lifecycle/android/build.gradle
index ba3a54b..5a584b4 100644
--- a/packages/flutter_plugin_android_lifecycle/android/build.gradle
+++ b/packages/flutter_plugin_android_lifecycle/android/build.gradle
@@ -31,6 +31,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
 
     dependencies {
diff --git a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle
index 6c5ea76..e3cf6ff 100644
--- a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle
+++ b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle
@@ -30,6 +30,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
 
     dependencies {
diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md
index e4207de..8ac07ae 100644
--- a/packages/google_sign_in/google_sign_in/CHANGELOG.md
+++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md
@@ -1,3 +1,7 @@
+## NEXT
+
+* Updated Android lint settings.
+
 ## 5.0.7
 
 * Mark iOS arm64 simulators as unsupported.
diff --git a/packages/google_sign_in/google_sign_in/android/build.gradle b/packages/google_sign_in/google_sign_in/android/build.gradle
index 7d1825d..ea98b31 100644
--- a/packages/google_sign_in/google_sign_in/android/build.gradle
+++ b/packages/google_sign_in/google_sign_in/android/build.gradle
@@ -30,6 +30,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
 
 
diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md
index 9d89389..4f21ed3 100644
--- a/packages/image_picker/image_picker/CHANGELOG.md
+++ b/packages/image_picker/image_picker/CHANGELOG.md
@@ -1,3 +1,7 @@
+## NEXT
+
+* Updated Android lint settings.
+
 ## 0.8.3+2
 
 * Fix using Camera as image source on Android 11+
diff --git a/packages/image_picker/image_picker/android/build.gradle b/packages/image_picker/image_picker/android/build.gradle
index 607b3c1..1e6439e 100755
--- a/packages/image_picker/image_picker/android/build.gradle
+++ b/packages/image_picker/image_picker/android/build.gradle
@@ -30,6 +30,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
     dependencies {
         implementation 'androidx.core:core:1.0.2'
diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md
index d67d1ef..60dae1b 100644
--- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md
+++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md
@@ -1,8 +1,12 @@
-# 0.1.4+4
+## NEXT
+
+* Updated Android lint settings.
+
+## 0.1.4+4
 
 * Removed dependency on the `test` package.
 
-# 0.1.4+3
+## 0.1.4+3
 
 - Updated installation instructions in README.
 
diff --git a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle
index 349f9ee..656f7c3 100644
--- a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle
+++ b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle
@@ -30,6 +30,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_8
diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md
index c33fa77..c0d04fb 100644
--- a/packages/local_auth/CHANGELOG.md
+++ b/packages/local_auth/CHANGELOG.md
@@ -1,3 +1,7 @@
+## NEXT
+
+* Updated Android lint settings.
+
 ## 1.1.7
 
 * Remove references to the Android V1 embedding.
diff --git a/packages/local_auth/android/build.gradle b/packages/local_auth/android/build.gradle
index 4fcb77c..dc282e7 100644
--- a/packages/local_auth/android/build.gradle
+++ b/packages/local_auth/android/build.gradle
@@ -30,6 +30,8 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
+        baseline file("lint-baseline.xml")
     }
 
 
diff --git a/packages/local_auth/android/lint-baseline.xml b/packages/local_auth/android/lint-baseline.xml
new file mode 100644
index 0000000..e89eaad
--- /dev/null
+++ b/packages/local_auth/android/lint-baseline.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.1" client="gradle" variant="debug" version="4.1.1">
+
+    <issue
+        id="NewApi"
+        message="`@android:style/Theme.Material.Dialog.Alert` requires API level 21 (current min is 16)"
+        errorLine1="  &lt;style name=&quot;AlertDialogCustom&quot; parent=&quot;@android:style/Theme.Material.Dialog.Alert&quot;>"
+        errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/res/values/styles.xml"
+            line="3"
+            column="35"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="`android:colorAccent` requires API level 21 (current min is 16)"
+        errorLine1="    &lt;item name=&quot;android:colorAccent&quot;>#FF009688&lt;/item>"
+        errorLine2="          ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/res/values/styles.xml"
+            line="7"
+            column="11"/>
+    </issue>
+
+    <issue
+        id="OldTargetApi"
+        message="Not targeting the latest versions of Android; compatibility modes apply. Consider testing and updating this version. Consult the `android.os.Build.VERSION_CODES` javadoc for details."
+        errorLine1="    &lt;uses-sdk android:targetSdkVersion=&quot;29&quot;/>"
+        errorLine2="              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/AndroidManifest.xml"
+            line="3"
+            column="15"/>
+    </issue>
+
+    <issue
+        id="UseCompoundDrawables"
+        message="This tag and its children can be replaced by one `&lt;TextView/>` and a compound drawable"
+        errorLine1="  &lt;LinearLayout"
+        errorLine2="   ~~~~~~~~~~~~">
+        <location
+            file="src/main/res/layout/scan_fp.xml"
+            line="26"
+            column="4"/>
+    </issue>
+
+    <issue
+        id="ContentDescription"
+        message="Missing `contentDescription` attribute on image"
+        errorLine1="    &lt;ImageView"
+        errorLine2="     ~~~~~~~~~">
+        <location
+            file="src/main/res/layout/scan_fp.xml"
+            line="30"
+            column="6"/>
+    </issue>
+
+</issues>
diff --git a/packages/package_info/CHANGELOG.md b/packages/package_info/CHANGELOG.md
index 2ec20b3..0fe9117 100644
--- a/packages/package_info/CHANGELOG.md
+++ b/packages/package_info/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## NEXT
 
 * Remove references to the Android v1 embedding.
+* Updated Android lint settings.
 
 ## 2.0.2
 
diff --git a/packages/package_info/android/build.gradle b/packages/package_info/android/build.gradle
index d2846f2..e21d911 100644
--- a/packages/package_info/android/build.gradle
+++ b/packages/package_info/android/build.gradle
@@ -30,6 +30,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
 
 
diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md
index 5e08c52..ba7bb3d 100644
--- a/packages/path_provider/path_provider/CHANGELOG.md
+++ b/packages/path_provider/path_provider/CHANGELOG.md
@@ -1,3 +1,7 @@
+## NEXT
+
+* Updated Android lint settings.
+
 ## 2.0.3
 
 * Add iOS unit test target.
diff --git a/packages/path_provider/path_provider/android/build.gradle b/packages/path_provider/path_provider/android/build.gradle
index db2c79c..3458140 100644
--- a/packages/path_provider/path_provider/android/build.gradle
+++ b/packages/path_provider/path_provider/android/build.gradle
@@ -30,6 +30,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
     android {
         compileOptions {
diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md
index 5d040f4..9087c28 100644
--- a/packages/quick_actions/quick_actions/CHANGELOG.md
+++ b/packages/quick_actions/quick_actions/CHANGELOG.md
@@ -1,3 +1,7 @@
+## NEXT
+
+* Updated Android lint settings.
+
 ## 0.6.0+5
 
 * Support only calling initialize once.
diff --git a/packages/quick_actions/quick_actions/android/build.gradle b/packages/quick_actions/quick_actions/android/build.gradle
index 0bce642..ec3f84e 100644
--- a/packages/quick_actions/quick_actions/android/build.gradle
+++ b/packages/quick_actions/quick_actions/android/build.gradle
@@ -30,6 +30,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
 
     dependencies {
diff --git a/packages/sensors/CHANGELOG.md b/packages/sensors/CHANGELOG.md
index 5ac0943..acea470 100644
--- a/packages/sensors/CHANGELOG.md
+++ b/packages/sensors/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## NEXT
 
 * Remove references to the Android V1 embedding.
+* Updated Android lint settings.
 
 ## 2.0.3
 
diff --git a/packages/sensors/android/build.gradle b/packages/sensors/android/build.gradle
index a16ebd2..7e10877 100644
--- a/packages/sensors/android/build.gradle
+++ b/packages/sensors/android/build.gradle
@@ -30,6 +30,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
 
 
diff --git a/packages/share/CHANGELOG.md b/packages/share/CHANGELOG.md
index 9074f59..c9a468d 100644
--- a/packages/share/CHANGELOG.md
+++ b/packages/share/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## NEXT
 
 * Remove references to the Android V1 embedding.
+* Updated Android lint settings.
 
 ## 2.0.4
 
diff --git a/packages/share/android/build.gradle b/packages/share/android/build.gradle
index 1b95bf5..b2ea363 100644
--- a/packages/share/android/build.gradle
+++ b/packages/share/android/build.gradle
@@ -30,6 +30,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
 
     dependencies {
diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md
index 3476f4e..48abf9a 100644
--- a/packages/shared_preferences/shared_preferences/CHANGELOG.md
+++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## NEXT
 
 * Add iOS unit test target.
+* Updated Android lint settings.
 
 ## 2.0.6
 
diff --git a/packages/shared_preferences/shared_preferences/android/build.gradle b/packages/shared_preferences/shared_preferences/android/build.gradle
index 6a66eba..9284f1c 100644
--- a/packages/shared_preferences/shared_preferences/android/build.gradle
+++ b/packages/shared_preferences/shared_preferences/android/build.gradle
@@ -38,6 +38,8 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
+        baseline file("lint-baseline.xml")
     }
     dependencies {
         testImplementation 'junit:junit:4.12'
diff --git a/packages/shared_preferences/shared_preferences/android/lint-baseline.xml b/packages/shared_preferences/shared_preferences/android/lint-baseline.xml
new file mode 100644
index 0000000..6b2f35f
--- /dev/null
+++ b/packages/shared_preferences/shared_preferences/android/lint-baseline.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="gradle" variant="debug" version="4.1.0">
+
+    <issue
+        id="CommitPrefEdits"
+        message="`SharedPreferences.edit()` without a corresponding `commit()` or `apply()` call"
+        errorLine1="          commitAsync(preferences.edit().putBoolean(key, (boolean) call.argument(&quot;value&quot;)), result);"
+        errorLine2="                      ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java"
+            line="66"
+            column="23"/>
+    </issue>
+
+    <issue
+        id="CommitPrefEdits"
+        message="`SharedPreferences.edit()` without a corresponding `commit()` or `apply()` call"
+        errorLine1="          commitAsync(preferences.edit().putString(key, DOUBLE_PREFIX + doubleValueStr), result);"
+        errorLine2="                      ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java"
+            line="71"
+            column="23"/>
+    </issue>
+
+    <issue
+        id="CommitPrefEdits"
+        message="`SharedPreferences.edit()` without a corresponding `commit()` or `apply()` call"
+        errorLine1="                preferences"
+        errorLine2="                ^">
+        <location
+            file="src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java"
+            line="78"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="CommitPrefEdits"
+        message="`SharedPreferences.edit()` without a corresponding `commit()` or `apply()` call"
+        errorLine1="            commitAsync(preferences.edit().putLong(key, number.longValue()), result);"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java"
+            line="84"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="CommitPrefEdits"
+        message="`SharedPreferences.edit()` without a corresponding `commit()` or `apply()` call"
+        errorLine1="          commitAsync(preferences.edit().putString(key, value), result);"
+        errorLine2="                      ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java"
+            line="96"
+            column="23"/>
+    </issue>
+
+    <issue
+        id="CommitPrefEdits"
+        message="`SharedPreferences.edit()` without a corresponding `commit()` or `apply()` call"
+        errorLine1="              preferences.edit().putString(key, LIST_IDENTIFIER + encodeList(list)), result);"
+        errorLine2="              ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java"
+            line="101"
+            column="15"/>
+    </issue>
+
+    <issue
+        id="CommitPrefEdits"
+        message="`SharedPreferences.edit()` without a corresponding `commit()` or `apply()` call"
+        errorLine1="          commitAsync(preferences.edit().remove(key), result);"
+        errorLine2="                      ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java"
+            line="111"
+            column="23"/>
+    </issue>
+
+</issues>
diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md
index dc67a21..237f0b1 100644
--- a/packages/url_launcher/url_launcher/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher/CHANGELOG.md
@@ -1,3 +1,7 @@
+## NEXT
+
+* Updated Android lint settings.
+
 ## 6.0.10
 
 * Remove references to the Android v1 embedding.
diff --git a/packages/url_launcher/url_launcher/android/build.gradle b/packages/url_launcher/url_launcher/android/build.gradle
index 5dd7e77..d374d40 100644
--- a/packages/url_launcher/url_launcher/android/build.gradle
+++ b/packages/url_launcher/url_launcher/android/build.gradle
@@ -30,6 +30,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
 
 
diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md
index f202962..f07bb5f 100644
--- a/packages/video_player/video_player/CHANGELOG.md
+++ b/packages/video_player/video_player/CHANGELOG.md
@@ -1,3 +1,7 @@
+## NEXT
+
+* Updated Android lint settings.
+
 ## 2.1.14
 
 * Removed dependency on the `flutter_test` package.
diff --git a/packages/video_player/video_player/android/build.gradle b/packages/video_player/video_player/android/build.gradle
index f2f18bf..9d99844 100644
--- a/packages/video_player/video_player/android/build.gradle
+++ b/packages/video_player/video_player/android/build.gradle
@@ -35,6 +35,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
     android {
         compileOptions {
diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md
index df7d9cb..361bfd2 100644
--- a/packages/webview_flutter/webview_flutter/CHANGELOG.md
+++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md
@@ -1,3 +1,7 @@
+## NEXT
+
+* Updated Android lint settings.
+
 ## 2.0.12
 
 * Improved the documentation on using the different Android Platform View modes.
diff --git a/packages/webview_flutter/webview_flutter/android/build.gradle b/packages/webview_flutter/webview_flutter/android/build.gradle
index cd1b418..4a16431 100644
--- a/packages/webview_flutter/webview_flutter/android/build.gradle
+++ b/packages/webview_flutter/webview_flutter/android/build.gradle
@@ -31,6 +31,7 @@
 
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
 
     dependencies {
diff --git a/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md
index 925745f..86f3f67 100644
--- a/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md
+++ b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md
@@ -1,3 +1,7 @@
+## NEXT
+
+* Updated Android lint settings.
+
 ## 2.0.2
 
 * Update README to point to Plus Plugins version.
diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle b/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle
index 2b5a8a7..661ee82 100644
--- a/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle
+++ b/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle
@@ -29,6 +29,7 @@
     }
     lintOptions {
         disable 'InvalidPackage'
+        disable 'GradleDependency'
     }
 
 
diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md
index 267019f..87917d6 100644
--- a/script/tool/CHANGELOG.md
+++ b/script/tool/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## NEXT
 
 - Added Android native integration test support to `native-test`.
+- Added a new `android-lint` command to lint Android plugin native code.
 
 ## 0.5.0
 
diff --git a/script/tool/lib/src/common/gradle.dart b/script/tool/lib/src/common/gradle.dart
new file mode 100644
index 0000000..e7214bf
--- /dev/null
+++ b/script/tool/lib/src/common/gradle.dart
@@ -0,0 +1,57 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/file.dart';
+import 'package:platform/platform.dart';
+
+import 'process_runner.dart';
+
+const String _gradleWrapperWindows = 'gradlew.bat';
+const String _gradleWrapperNonWindows = 'gradlew';
+
+/// A utility class for interacting with Gradle projects.
+class GradleProject {
+  /// Creates an instance that runs commands for [project] with the given
+  /// [processRunner].
+  ///
+  /// If [log] is true, commands run by this instance will long various status
+  /// messages.
+  GradleProject(
+    this.flutterProject, {
+    this.processRunner = const ProcessRunner(),
+    this.platform = const LocalPlatform(),
+  });
+
+  /// The directory of a Flutter project to run Gradle commands in.
+  final Directory flutterProject;
+
+  /// The [ProcessRunner] used to run commands. Overridable for testing.
+  final ProcessRunner processRunner;
+
+  /// The platform that commands are being run on.
+  final Platform platform;
+
+  /// The project's 'android' directory.
+  Directory get androidDirectory => flutterProject.childDirectory('android');
+
+  /// The path to the Gradle wrapper file for the project.
+  File get gradleWrapper => androidDirectory.childFile(
+      platform.isWindows ? _gradleWrapperWindows : _gradleWrapperNonWindows);
+
+  /// Whether or not the project is ready to have Gradle commands run on it
+  /// (i.e., whether the `flutter` tool has generated the necessary files).
+  bool isConfigured() => gradleWrapper.existsSync();
+
+  /// Runs a `gradlew` command with the given parameters.
+  Future<int> runCommand(
+    String target, {
+    List<String> arguments = const <String>[],
+  }) {
+    return processRunner.runAndStream(
+      gradleWrapper.path,
+      <String>[target, ...arguments],
+      workingDir: androidDirectory,
+    );
+  }
+}
diff --git a/script/tool/lib/src/common/xcode.dart b/script/tool/lib/src/common/xcode.dart
index d6bbae4..83f681b 100644
--- a/script/tool/lib/src/common/xcode.dart
+++ b/script/tool/lib/src/common/xcode.dart
@@ -15,7 +15,7 @@
 
 /// A utility class for interacting with the installed version of Xcode.
 class Xcode {
-  /// Creates an instance that runs commends with the given [processRunner].
+  /// Creates an instance that runs commands with the given [processRunner].
   ///
   /// If [log] is true, commands run by this instance will long various status
   /// messages.
diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart
index 8459f6c..fd2de97 100644
--- a/script/tool/lib/src/firebase_test_lab_command.dart
+++ b/script/tool/lib/src/firebase_test_lab_command.dart
@@ -10,6 +10,7 @@
 import 'package:uuid/uuid.dart';
 
 import 'common/core.dart';
+import 'common/gradle.dart';
 import 'common/package_looping_command.dart';
 import 'common/process_runner.dart';
 
@@ -74,8 +75,6 @@
       'Runs tests in test_instrumentation folder using the '
       'instrumentation_test package.';
 
-  static const String _gradleWrapper = 'gradlew';
-
   bool _firebaseProjectConfigured = false;
 
   Future<void> _configureFirebaseProject() async {
@@ -138,13 +137,15 @@
     }
 
     // Ensures that gradle wrapper exists
-    if (!await _ensureGradleWrapperExists(androidDirectory)) {
+    final GradleProject project = GradleProject(exampleDirectory,
+        processRunner: processRunner, platform: platform);
+    if (!await _ensureGradleWrapperExists(project)) {
       return PackageResult.fail(<String>['Unable to build example apk']);
     }
 
     await _configureFirebaseProject();
 
-    if (!await _runGradle(androidDirectory, 'app:assembleAndroidTest')) {
+    if (!await _runGradle(project, 'app:assembleAndroidTest')) {
       return PackageResult.fail(<String>['Unable to assemble androidTest']);
     }
 
@@ -156,8 +157,7 @@
     for (final File test in _findIntegrationTestFiles(package)) {
       final String testName = getRelativePosixPath(test, from: package);
       print('Testing $testName...');
-      if (!await _runGradle(androidDirectory, 'app:assembleDebug',
-          testFile: test)) {
+      if (!await _runGradle(project, 'app:assembleDebug', testFile: test)) {
         printError('Could not build $testName');
         errors.add('$testName failed to build');
         continue;
@@ -204,12 +204,12 @@
         : PackageResult.fail(errors);
   }
 
-  /// Checks that 'gradlew' exists in [androidDirectory], and if not runs a
+  /// Checks that Gradle has been configured for [project], and if not runs a
   /// Flutter build to generate it.
   ///
   /// Returns true if either gradlew was already present, or the build succeeds.
-  Future<bool> _ensureGradleWrapperExists(Directory androidDirectory) async {
-    if (!androidDirectory.childFile(_gradleWrapper).existsSync()) {
+  Future<bool> _ensureGradleWrapperExists(GradleProject project) async {
+    if (!project.isConfigured()) {
       print('Running flutter build apk...');
       final String experiment = getStringArg(kEnableExperiment);
       final int exitCode = await processRunner.runAndStream(
@@ -219,7 +219,7 @@
             'apk',
             if (experiment.isNotEmpty) '--enable-experiment=$experiment',
           ],
-          workingDir: androidDirectory);
+          workingDir: project.androidDirectory);
 
       if (exitCode != 0) {
         return false;
@@ -228,15 +228,15 @@
     return true;
   }
 
-  /// Builds [target] using 'gradlew' in the given [directory]. Assumes
-  /// 'gradlew' already exists.
+  /// Builds [target] using Gradle in the given [project]. Assumes Gradle is
+  /// already configured.
   ///
   /// [testFile] optionally does the Flutter build with the given test file as
   /// the build target.
   ///
   /// Returns true if the command succeeds.
   Future<bool> _runGradle(
-    Directory directory,
+    GradleProject project,
     String target, {
     File? testFile,
   }) async {
@@ -245,17 +245,15 @@
         ? Uri.encodeComponent('--enable-experiment=$experiment')
         : null;
 
-    final int exitCode = await processRunner.runAndStream(
-        directory.childFile(_gradleWrapper).path,
-        <String>[
-          target,
-          '-Pverbose=true',
-          if (testFile != null) '-Ptarget=${testFile.path}',
-          if (extraOptions != null) '-Pextra-front-end-options=$extraOptions',
-          if (extraOptions != null)
-            '-Pextra-gen-snapshot-options=$extraOptions',
-        ],
-        workingDir: directory);
+    final int exitCode = await project.runCommand(
+      target,
+      arguments: <String>[
+        '-Pverbose=true',
+        if (testFile != null) '-Ptarget=${testFile.path}',
+        if (extraOptions != null) '-Pextra-front-end-options=$extraOptions',
+        if (extraOptions != null) '-Pextra-gen-snapshot-options=$extraOptions',
+      ],
+    );
 
     if (exitCode != 0) {
       return false;
diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart
new file mode 100644
index 0000000..be6c6ed
--- /dev/null
+++ b/script/tool/lib/src/lint_android_command.dart
@@ -0,0 +1,61 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/file.dart';
+import 'package:flutter_plugin_tools/src/common/plugin_utils.dart';
+import 'package:platform/platform.dart';
+
+import 'common/core.dart';
+import 'common/gradle.dart';
+import 'common/package_looping_command.dart';
+import 'common/process_runner.dart';
+
+/// Lint the CocoaPod podspecs and run unit tests.
+///
+/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint.
+class LintAndroidCommand extends PackageLoopingCommand {
+  /// Creates an instance of the linter command.
+  LintAndroidCommand(
+    Directory packagesDir, {
+    ProcessRunner processRunner = const ProcessRunner(),
+    Platform platform = const LocalPlatform(),
+  }) : super(packagesDir, processRunner: processRunner, platform: platform);
+
+  @override
+  final String name = 'lint-android';
+
+  @override
+  final String description = 'Runs "gradlew lint" on Android plugins.\n\n'
+      'Requires the example to have been build at least once before running.';
+
+  @override
+  Future<PackageResult> runForPackage(Directory package) async {
+    if (!pluginSupportsPlatform(kPlatformAndroid, package,
+        requiredMode: PlatformSupport.inline)) {
+      return PackageResult.skip(
+          'Plugin does not have an Android implemenatation.');
+    }
+
+    final Directory exampleDirectory = package.childDirectory('example');
+    final GradleProject project = GradleProject(exampleDirectory,
+        processRunner: processRunner, platform: platform);
+
+    if (!project.isConfigured()) {
+      return PackageResult.fail(<String>['Build example before linting']);
+    }
+
+    final String packageName = package.basename;
+
+    // Only lint one build mode to avoid extra work.
+    // Only lint the plugin project itself, to avoid failing due to errors in
+    // dependencies.
+    //
+    // TODO(stuartmorgan): Consider adding an XML parser to read and summarize
+    // all results. Currently, only the first three errors will be shown inline,
+    // and the rest have to be checked via the CI-uploaded artifact.
+    final int exitCode = await project.runCommand('$packageName:lintDebug');
+
+    return exitCode == 0 ? PackageResult.success() : PackageResult.fail();
+  }
+}
diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart
index 6001c5d..e70cba2 100644
--- a/script/tool/lib/src/main.dart
+++ b/script/tool/lib/src/main.dart
@@ -16,6 +16,7 @@
 import 'firebase_test_lab_command.dart';
 import 'format_command.dart';
 import 'license_check_command.dart';
+import 'lint_android_command.dart';
 import 'lint_podspecs_command.dart';
 import 'list_command.dart';
 import 'native_test_command.dart';
@@ -51,6 +52,7 @@
     ..addCommand(FirebaseTestLabCommand(packagesDir))
     ..addCommand(FormatCommand(packagesDir))
     ..addCommand(LicenseCheckCommand(packagesDir))
+    ..addCommand(LintAndroidCommand(packagesDir))
     ..addCommand(LintPodspecsCommand(packagesDir))
     ..addCommand(ListCommand(packagesDir))
     ..addCommand(NativeTestCommand(packagesDir))
diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart
index 9fc6a29..0bd2ab4 100644
--- a/script/tool/lib/src/native_test_command.dart
+++ b/script/tool/lib/src/native_test_command.dart
@@ -6,6 +6,7 @@
 import 'package:platform/platform.dart';
 
 import 'common/core.dart';
+import 'common/gradle.dart';
 import 'common/package_looping_command.dart';
 import 'common/plugin_utils.dart';
 import 'common/process_runner.dart';
@@ -47,8 +48,6 @@
         help: 'Runs native integration (UI) tests', defaultsTo: true);
   }
 
-  static const String _gradleWrapper = 'gradlew';
-
   // The device destination flags for iOS tests.
   List<String> _iosDestinationFlags = <String>[];
 
@@ -243,9 +242,12 @@
       final String exampleName = getPackageDescription(example);
       _printRunningExampleTestsMessage(example, 'Android');
 
-      final Directory androidDirectory = example.childDirectory('android');
-      final File gradleFile = androidDirectory.childFile(_gradleWrapper);
-      if (!gradleFile.existsSync()) {
+      final GradleProject project = GradleProject(
+        example,
+        processRunner: processRunner,
+        platform: platform,
+      );
+      if (!project.isConfigured()) {
         printError('ERROR: Run "flutter build apk" on $exampleName, or run '
             'this tool\'s "build-examples --apk" command, '
             'before executing tests.');
@@ -256,9 +258,7 @@
 
       if (runUnitTests) {
         print('Running unit tests...');
-        final int exitCode = await processRunner.runAndStream(
-            gradleFile.path, <String>['testDebugUnitTest'],
-            workingDir: androidDirectory);
+        final int exitCode = await project.runCommand('testDebugUnitTest');
         if (exitCode != 0) {
           printError('$exampleName unit tests failed.');
           failed = true;
@@ -275,13 +275,12 @@
             'notAnnotation=io.flutter.plugins.DartIntegrationTest';
 
         print('Running integration tests...');
-        final int exitCode = await processRunner.runAndStream(
-            gradleFile.path,
-            <String>[
-              'app:connectedAndroidTest',
-              '-Pandroid.testInstrumentationRunnerArguments.$filter',
-            ],
-            workingDir: androidDirectory);
+        final int exitCode = await project.runCommand(
+          'app:connectedAndroidTest',
+          arguments: <String>[
+            '-Pandroid.testInstrumentationRunnerArguments.$filter',
+          ],
+        );
         if (exitCode != 0) {
           printError('$exampleName integration tests failed.');
           failed = true;
diff --git a/script/tool/test/common/gradle_test.dart b/script/tool/test/common/gradle_test.dart
new file mode 100644
index 0000000..c24887d
--- /dev/null
+++ b/script/tool/test/common/gradle_test.dart
@@ -0,0 +1,179 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io' as io;
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_plugin_tools/src/common/gradle.dart';
+import 'package:test/test.dart';
+
+import '../mocks.dart';
+import '../util.dart';
+
+void main() {
+  late FileSystem fileSystem;
+  late RecordingProcessRunner processRunner;
+
+  setUp(() {
+    fileSystem = MemoryFileSystem();
+    processRunner = RecordingProcessRunner();
+  });
+
+  group('isConfigured', () {
+    test('reports true when configured on Windows', () async {
+      final Directory plugin = createFakePlugin(
+          'plugin', fileSystem.directory('/'),
+          extraFiles: <String>['android/gradlew.bat']);
+      final GradleProject project = GradleProject(
+        plugin,
+        processRunner: processRunner,
+        platform: MockPlatform(isWindows: true),
+      );
+
+      expect(project.isConfigured(), true);
+    });
+
+    test('reports true when configured on non-Windows', () async {
+      final Directory plugin = createFakePlugin(
+          'plugin', fileSystem.directory('/'),
+          extraFiles: <String>['android/gradlew']);
+      final GradleProject project = GradleProject(
+        plugin,
+        processRunner: processRunner,
+        platform: MockPlatform(isMacOS: true),
+      );
+
+      expect(project.isConfigured(), true);
+    });
+
+    test('reports false when not configured on Windows', () async {
+      final Directory plugin = createFakePlugin(
+          'plugin', fileSystem.directory('/'),
+          extraFiles: <String>['android/foo']);
+      final GradleProject project = GradleProject(
+        plugin,
+        processRunner: processRunner,
+        platform: MockPlatform(isWindows: true),
+      );
+
+      expect(project.isConfigured(), false);
+    });
+
+    test('reports true when configured on non-Windows', () async {
+      final Directory plugin = createFakePlugin(
+          'plugin', fileSystem.directory('/'),
+          extraFiles: <String>['android/foo']);
+      final GradleProject project = GradleProject(
+        plugin,
+        processRunner: processRunner,
+        platform: MockPlatform(isMacOS: true),
+      );
+
+      expect(project.isConfigured(), false);
+    });
+  });
+
+  group('runXcodeBuild', () {
+    test('runs without arguments', () async {
+      final Directory plugin = createFakePlugin(
+          'plugin', fileSystem.directory('/'),
+          extraFiles: <String>['android/gradlew']);
+      final GradleProject project = GradleProject(
+        plugin,
+        processRunner: processRunner,
+        platform: MockPlatform(isMacOS: true),
+      );
+
+      final int exitCode = await project.runCommand('foo');
+
+      expect(exitCode, 0);
+      expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            ProcessCall(
+                plugin.childDirectory('android').childFile('gradlew').path,
+                const <String>[
+                  'foo',
+                ],
+                plugin.childDirectory('android').path),
+          ]));
+    });
+
+    test('runs with arguments', () async {
+      final Directory plugin = createFakePlugin(
+          'plugin', fileSystem.directory('/'),
+          extraFiles: <String>['android/gradlew']);
+      final GradleProject project = GradleProject(
+        plugin,
+        processRunner: processRunner,
+        platform: MockPlatform(isMacOS: true),
+      );
+
+      final int exitCode = await project.runCommand(
+        'foo',
+        arguments: <String>['--bar', '--baz'],
+      );
+
+      expect(exitCode, 0);
+      expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            ProcessCall(
+                plugin.childDirectory('android').childFile('gradlew').path,
+                const <String>[
+                  'foo',
+                  '--bar',
+                  '--baz',
+                ],
+                plugin.childDirectory('android').path),
+          ]));
+    });
+
+    test('runs with the correct wrapper on Windows', () async {
+      final Directory plugin = createFakePlugin(
+          'plugin', fileSystem.directory('/'),
+          extraFiles: <String>['android/gradlew.bat']);
+      final GradleProject project = GradleProject(
+        plugin,
+        processRunner: processRunner,
+        platform: MockPlatform(isWindows: true),
+      );
+
+      final int exitCode = await project.runCommand('foo');
+
+      expect(exitCode, 0);
+      expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            ProcessCall(
+                plugin.childDirectory('android').childFile('gradlew.bat').path,
+                const <String>[
+                  'foo',
+                ],
+                plugin.childDirectory('android').path),
+          ]));
+    });
+
+    test('returns error codes', () async {
+      final Directory plugin = createFakePlugin(
+          'plugin', fileSystem.directory('/'),
+          extraFiles: <String>['android/gradlew.bat']);
+      final GradleProject project = GradleProject(
+        plugin,
+        processRunner: processRunner,
+        platform: MockPlatform(isWindows: true),
+      );
+
+      processRunner.mockProcessesForExecutable[project.gradleWrapper.path] =
+          <io.Process>[
+        MockProcess.failing(),
+      ];
+
+      final int exitCode = await project.runCommand('foo');
+
+      expect(exitCode, 1);
+    });
+  });
+}
diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart
new file mode 100644
index 0000000..05ead22
--- /dev/null
+++ b/script/tool/test/lint_android_command_test.dart
@@ -0,0 +1,158 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io' as io;
+
+import 'package:args/command_runner.dart';
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_plugin_tools/src/common/core.dart';
+import 'package:flutter_plugin_tools/src/common/plugin_utils.dart';
+import 'package:flutter_plugin_tools/src/lint_android_command.dart';
+import 'package:test/test.dart';
+
+import 'mocks.dart';
+import 'util.dart';
+
+void main() {
+  group('$LintAndroidCommand', () {
+    FileSystem fileSystem;
+    late Directory packagesDir;
+    late CommandRunner<void> runner;
+    late MockPlatform mockPlatform;
+    late RecordingProcessRunner processRunner;
+
+    setUp(() {
+      fileSystem = MemoryFileSystem(style: FileSystemStyle.posix);
+      packagesDir = createPackagesDirectory(fileSystem: fileSystem);
+      mockPlatform = MockPlatform();
+      processRunner = RecordingProcessRunner();
+      final LintAndroidCommand command = LintAndroidCommand(
+        packagesDir,
+        processRunner: processRunner,
+        platform: mockPlatform,
+      );
+
+      runner = CommandRunner<void>(
+          'lint_android_test', 'Test for $LintAndroidCommand');
+      runner.addCommand(command);
+    });
+
+    test('runs gradle lint', () async {
+      final Directory pluginDir =
+          createFakePlugin('plugin1', packagesDir, extraFiles: <String>[
+        'example/android/gradlew',
+      ], platformSupport: <String, PlatformSupport>{
+        kPlatformAndroid: PlatformSupport.inline
+      });
+
+      final Directory androidDir =
+          pluginDir.childDirectory('example').childDirectory('android');
+
+      final List<String> output =
+          await runCapturingPrint(runner, <String>['lint-android']);
+
+      expect(
+        processRunner.recordedCalls,
+        orderedEquals(<ProcessCall>[
+          ProcessCall(
+            androidDir.childFile('gradlew').path,
+            const <String>['plugin1:lintDebug'],
+            androidDir.path,
+          ),
+        ]),
+      );
+
+      expect(
+          output,
+          containsAllInOrder(<Matcher>[
+            contains('Running for plugin1'),
+            contains('No issues found!'),
+          ]));
+    });
+
+    test('fails if gradlew is missing', () async {
+      createFakePlugin('plugin1', packagesDir,
+          platformSupport: <String, PlatformSupport>{
+            kPlatformAndroid: PlatformSupport.inline
+          });
+
+      Error? commandError;
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['lint-android'], errorHandler: (Error e) {
+        commandError = e;
+      });
+
+      expect(commandError, isA<ToolExit>());
+      expect(
+          output,
+          containsAllInOrder(
+            <Matcher>[
+              contains('Build example before linting'),
+            ],
+          ));
+    });
+
+    test('fails if linting finds issues', () async {
+      createFakePlugin('plugin1', packagesDir,
+          platformSupport: <String, PlatformSupport>{
+            kPlatformAndroid: PlatformSupport.inline
+          });
+
+      processRunner.mockProcessesForExecutable['gradlew'] = <io.Process>[
+        MockProcess.failing(),
+      ];
+
+      Error? commandError;
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['lint-android'], errorHandler: (Error e) {
+        commandError = e;
+      });
+
+      expect(commandError, isA<ToolExit>());
+      expect(
+          output,
+          containsAllInOrder(
+            <Matcher>[
+              contains('Build example before linting'),
+            ],
+          ));
+    });
+
+    test('skips non-Android plugins', () async {
+      createFakePlugin('plugin1', packagesDir);
+
+      final List<String> output =
+          await runCapturingPrint(runner, <String>['lint-android']);
+
+      expect(
+          output,
+          containsAllInOrder(
+            <Matcher>[
+              contains(
+                  'SKIPPING: Plugin does not have an Android implemenatation.')
+            ],
+          ));
+    });
+
+    test('skips non-inline plugins', () async {
+      createFakePlugin('plugin1', packagesDir,
+          platformSupport: <String, PlatformSupport>{
+            kPlatformAndroid: PlatformSupport.federated
+          });
+
+      final List<String> output =
+          await runCapturingPrint(runner, <String>['lint-android']);
+
+      expect(
+          output,
+          containsAllInOrder(
+            <Matcher>[
+              contains(
+                  'SKIPPING: Plugin does not have an Android implemenatation.')
+            ],
+          ));
+    });
+  });
+}