summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/aapt/Command.cpp6
-rw-r--r--tools/aapt/ResourceTable.cpp8
-rw-r--r--tools/aapt2/Android.mk2
-rw-r--r--tools/aapt2/ResourceUtils.cpp9
-rw-r--r--tools/aapt2/SdkConstants.cpp2
-rw-r--r--tools/aapt2/cmd/Link.h2
-rw-r--r--tools/aapt2/format/Container.cpp4
-rw-r--r--tools/aapt2/io/Util.h3
-rw-r--r--tools/aapt2/link/ManifestFixer.cpp17
-rw-r--r--tools/aapt2/link/ManifestFixer.h3
-rw-r--r--tools/aapt2/link/ManifestFixer_test.cpp48
-rw-r--r--tools/bit/adb.cpp4
-rw-r--r--tools/bit/main.cpp126
-rw-r--r--tools/codegen/Android.bp2
-rw-r--r--tools/codegen/manifest.txt1
-rw-r--r--tools/fonts/Android.bp5
-rw-r--r--tools/lint/OWNERS4
-rw-r--r--tools/lint/README.md48
-rw-r--r--tools/lint/checks/src/main/java/com/google/android/lint/EnforcePermissionDetector.kt169
-rw-r--r--tools/lint/checks/src/test/java/com/google/android/lint/EnforcePermissionDetectorTest.kt202
-rw-r--r--tools/lint/common/Android.bp29
-rw-r--r--tools/lint/common/src/main/java/com/google/android/lint/Constants.kt40
-rw-r--r--tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt52
-rw-r--r--tools/lint/common/src/main/java/com/google/android/lint/model/Method.kt26
-rw-r--r--tools/lint/fix/Android.bp33
-rw-r--r--tools/lint/fix/README.md30
-rw-r--r--tools/lint/fix/soong_lint_fix.py173
-rw-r--r--tools/lint/framework/Android.bp (renamed from tools/lint/Android.bp)20
-rw-r--r--tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt55
-rw-r--r--tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt (renamed from tools/lint/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt)375
-rw-r--r--tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt (renamed from tools/lint/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt)0
-rw-r--r--tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt515
-rw-r--r--tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt199
-rw-r--r--tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt229
-rw-r--r--tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt42
-rw-r--r--tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt126
-rw-r--r--tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt (renamed from tools/lint/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt)241
-rw-r--r--tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt (renamed from tools/lint/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt)0
-rw-r--r--tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt271
-rw-r--r--tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt823
-rw-r--r--tools/lint/global/Android.bp65
-rw-r--r--tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt (renamed from tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt)22
-rw-r--r--tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt52
-rw-r--r--tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt76
-rw-r--r--tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt226
-rw-r--r--tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt384
-rw-r--r--tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt149
-rw-r--r--tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt96
-rw-r--r--tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt92
-rw-r--r--tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorCodegenTest.kt557
-rw-r--r--tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt425
-rw-r--r--tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorCodegenTest.kt557
-rw-r--r--tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt443
-rw-r--r--tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt843
-rw-r--r--tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt88
-rw-r--r--tools/localedata/OWNERS2
-rwxr-xr-xtools/localedata/extract_icu_data.py68
-rw-r--r--tools/locked_region_code_injection/Android.bp27
-rw-r--r--tools/locked_region_code_injection/OWNERS4
-rw-r--r--tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java196
-rw-r--r--tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java37
-rw-r--r--tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java9
-rw-r--r--tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java23
-rw-r--r--tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java25
-rw-r--r--tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java166
-rw-r--r--tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java38
-rw-r--r--tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java56
-rw-r--r--tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java12
-rw-r--r--tools/locked_region_code_injection/test/manifest.txt1
-rwxr-xr-xtools/locked_region_code_injection/test/unit-test.sh98
-rw-r--r--tools/preload-check/AndroidTest.xml2
-rw-r--r--tools/preload-check/OWNERS1
-rw-r--r--tools/preload/loadclass/LoadClass.java8
-rw-r--r--tools/processors/intdef_mappings/Android.bp22
-rw-r--r--tools/processors/staledataclass/Android.bp18
-rw-r--r--tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt3
-rw-r--r--tools/processors/view_inspector/Android.bp18
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt2
-rw-r--r--tools/sdkparcelables/Android.bp2
-rw-r--r--tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt2
-rw-r--r--tools/stringslint/stringslint.py234
-rwxr-xr-xtools/stringslint/stringslint_sha.sh6
-rw-r--r--tools/traceinjection/Android.bp10
-rw-r--r--tools/traceinjection/src/com/android/traceinjection/TraceInjectionClassVisitor.java2
-rw-r--r--tools/traceinjection/src/com/android/traceinjection/TraceInjectionMethodAdapter.java4
-rw-r--r--tools/validatekeymaps/OWNERS3
86 files changed, 8017 insertions, 1101 deletions
diff --git a/tools/aapt/Command.cpp b/tools/aapt/Command.cpp
index fecc7b3cbf37..d02fd83df6af 100644
--- a/tools/aapt/Command.cpp
+++ b/tools/aapt/Command.cpp
@@ -1028,7 +1028,6 @@ int doDump(Bundle* bundle)
// These permissions are required by services implementing services
// the system binds to (IME, Accessibility, PrintServices, etc.)
bool hasBindDeviceAdminPermission = false;
- bool hasBindInputMethodPermission = false;
bool hasBindAccessibilityServicePermission = false;
bool hasBindPrintServicePermission = false;
bool hasBindNfcServicePermission = false;
@@ -1757,7 +1756,6 @@ int doDump(Bundle* bundle)
hasMetaHostPaymentCategory = false;
hasMetaOffHostPaymentCategory = false;
hasBindDeviceAdminPermission = false;
- hasBindInputMethodPermission = false;
hasBindAccessibilityServicePermission = false;
hasBindPrintServicePermission = false;
hasBindNfcServicePermission = false;
@@ -1871,9 +1869,7 @@ int doDump(Bundle* bundle)
String8 permission = AaptXml::getAttribute(tree, PERMISSION_ATTR,
&error);
if (error == "") {
- if (permission == "android.permission.BIND_INPUT_METHOD") {
- hasBindInputMethodPermission = true;
- } else if (permission ==
+ if (permission ==
"android.permission.BIND_ACCESSIBILITY_SERVICE") {
hasBindAccessibilityServicePermission = true;
} else if (permission ==
diff --git a/tools/aapt/ResourceTable.cpp b/tools/aapt/ResourceTable.cpp
index b9de11b0026b..47750fc11a6e 100644
--- a/tools/aapt/ResourceTable.cpp
+++ b/tools/aapt/ResourceTable.cpp
@@ -2970,14 +2970,6 @@ status_t ResourceTable::flatten(Bundle* bundle, const sp<const ResourceFilter>&
}
e->setNameIndex(keyStrings.add(e->getName(), true));
- // If this entry has no values for other configs,
- // and is the default config, then it is special. Otherwise
- // we want to add it with the config info.
- ConfigDescription* valueConfig = NULL;
- if (N != 1 || config == nullConfig) {
- valueConfig = &config;
- }
-
status_t err = e->prepareFlatten(&valueStrings, this,
&configTypeName, &config);
if (err != NO_ERROR) {
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk
index 7b94e718fd0e..34a1b112d880 100644
--- a/tools/aapt2/Android.mk
+++ b/tools/aapt2/Android.mk
@@ -15,7 +15,7 @@ $(aapt2_results): .KATI_IMPLICIT_OUTPUTS := $(aapt2_results)-nocache
$(aapt2_results): $(HOST_OUT_NATIVE_TESTS)/aapt2_tests/aapt2_tests
-$(HOST_OUT_NATIVE_TESTS)/aapt2_tests/aapt2_tests --gtest_output=xml:$@ > /dev/null 2>&1
-$(call declare-0p-target,$(aapt2_results))
+$(call declare-1p-target,$(aapt2_results))
aapt2_results :=
diff --git a/tools/aapt2/ResourceUtils.cpp b/tools/aapt2/ResourceUtils.cpp
index 23f6c88aad91..3787f3b96f08 100644
--- a/tools/aapt2/ResourceUtils.cpp
+++ b/tools/aapt2/ResourceUtils.cpp
@@ -51,8 +51,10 @@ std::optional<ResourceName> ToResourceName(const android::ResTable::resource_nam
util::Utf16ToUtf8(StringPiece16(name_in.package, name_in.packageLen));
std::optional<ResourceNamedTypeRef> type;
+ std::string converted;
if (name_in.type) {
- type = ParseResourceNamedType(util::Utf16ToUtf8(StringPiece16(name_in.type, name_in.typeLen)));
+ converted = util::Utf16ToUtf8(StringPiece16(name_in.type, name_in.typeLen));
+ type = ParseResourceNamedType(converted);
} else if (name_in.type8) {
type = ParseResourceNamedType(StringPiece(name_in.type8, name_in.typeLen));
} else {
@@ -85,9 +87,10 @@ std::optional<ResourceName> ToResourceName(const android::AssetManager2::Resourc
name_out.package = std::string(name_in.package, name_in.package_len);
std::optional<ResourceNamedTypeRef> type;
+ std::string converted;
if (name_in.type16) {
- type =
- ParseResourceNamedType(util::Utf16ToUtf8(StringPiece16(name_in.type16, name_in.type_len)));
+ converted = util::Utf16ToUtf8(StringPiece16(name_in.type16, name_in.type_len));
+ type = ParseResourceNamedType(converted);
} else if (name_in.type) {
type = ParseResourceNamedType(StringPiece(name_in.type, name_in.type_len));
} else {
diff --git a/tools/aapt2/SdkConstants.cpp b/tools/aapt2/SdkConstants.cpp
index 8ea43abff895..34e8edb0a47f 100644
--- a/tools/aapt2/SdkConstants.cpp
+++ b/tools/aapt2/SdkConstants.cpp
@@ -27,7 +27,7 @@ namespace aapt {
static ApiVersion sDevelopmentSdkLevel = 10000;
static const auto sDevelopmentSdkCodeNames =
- std::unordered_set<StringPiece>({"Q", "R", "S", "Sv2", "Tiramisu"});
+ std::unordered_set<StringPiece>({"Q", "R", "S", "Sv2", "Tiramisu", "UpsideDownCake"});
static const std::vector<std::pair<uint16_t, ApiVersion>> sAttrIdMap = {
{0x021c, 1},
diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h
index d8c76e297ec5..0170c4a4c54b 100644
--- a/tools/aapt2/cmd/Link.h
+++ b/tools/aapt2/cmd/Link.h
@@ -270,6 +270,8 @@ class LinkCommand : public Command {
"Changes the name of the target package for overlay. Most useful\n"
"when used in conjunction with --rename-manifest-package.",
&options_.manifest_fixer_options.rename_overlay_target_package);
+ AddOptionalFlag("--rename-overlay-category", "Changes the category for the overlay.",
+ &options_.manifest_fixer_options.rename_overlay_category);
AddOptionalFlagList("-0", "File suffix not to compress.",
&options_.extensions_to_not_compress);
AddOptionalSwitch("--no-compress", "Do not compress any resources.",
diff --git a/tools/aapt2/format/Container.cpp b/tools/aapt2/format/Container.cpp
index 9cef7b3d2ce3..1ff6c4996b91 100644
--- a/tools/aapt2/format/Container.cpp
+++ b/tools/aapt2/format/Container.cpp
@@ -76,7 +76,7 @@ bool ContainerWriter::AddResTableEntry(const pb::ResourceTable& table) {
coded_out.WriteLittleEndian32(kResTable);
// Write the aligned size.
- const ::google::protobuf::uint64 size = table.ByteSize();
+ const size_t size = table.ByteSizeLong();
const int padding = CalculatePaddingForAlignment(size);
coded_out.WriteLittleEndian64(size);
@@ -109,7 +109,7 @@ bool ContainerWriter::AddResFileEntry(const pb::internal::CompiledFile& file,
coded_out.WriteLittleEndian32(kResFile);
// Write the aligned size.
- const ::google::protobuf::uint32 header_size = file.ByteSize();
+ const size_t header_size = file.ByteSizeLong();
const int header_padding = CalculatePaddingForAlignment(header_size);
const ::google::protobuf::uint64 data_size = in->TotalSize();
const int data_padding = CalculatePaddingForAlignment(data_size);
diff --git a/tools/aapt2/io/Util.h b/tools/aapt2/io/Util.h
index 5cb8206db23c..1b48a288d255 100644
--- a/tools/aapt2/io/Util.h
+++ b/tools/aapt2/io/Util.h
@@ -131,8 +131,7 @@ class ProtoInputStreamReader {
template <typename T> bool ReadMessage(T *message) {
ZeroCopyInputAdaptor adapter(in_);
google::protobuf::io::CodedInputStream coded_stream(&adapter);
- coded_stream.SetTotalBytesLimit(std::numeric_limits<int32_t>::max(),
- coded_stream.BytesUntilTotalBytesLimit());
+ coded_stream.SetTotalBytesLimit(std::numeric_limits<int32_t>::max());
return message->ParseFromCodedStream(&coded_stream);
}
diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp
index 948b11b6795e..42191912775a 100644
--- a/tools/aapt2/link/ManifestFixer.cpp
+++ b/tools/aapt2/link/ManifestFixer.cpp
@@ -449,13 +449,18 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor,
manifest_action["attribution"]["inherit-from"];
manifest_action["original-package"];
manifest_action["overlay"].Action([&](xml::Element* el) -> bool {
- if (!options_.rename_overlay_target_package) {
- return true;
+ if (options_.rename_overlay_target_package) {
+ if (xml::Attribute* attr = el->FindAttribute(xml::kSchemaAndroid, "targetPackage")) {
+ attr->value = options_.rename_overlay_target_package.value();
+ }
}
-
- if (xml::Attribute* attr =
- el->FindAttribute(xml::kSchemaAndroid, "targetPackage")) {
- attr->value = options_.rename_overlay_target_package.value();
+ if (options_.rename_overlay_category) {
+ if (xml::Attribute* attr = el->FindAttribute(xml::kSchemaAndroid, "category")) {
+ attr->value = options_.rename_overlay_category.value();
+ } else {
+ el->attributes.push_back(xml::Attribute{xml::kSchemaAndroid, "category",
+ options_.rename_overlay_category.value()});
+ }
}
return true;
});
diff --git a/tools/aapt2/link/ManifestFixer.h b/tools/aapt2/link/ManifestFixer.h
index d5d1d1770e1c..a8707d9d8623 100644
--- a/tools/aapt2/link/ManifestFixer.h
+++ b/tools/aapt2/link/ManifestFixer.h
@@ -48,6 +48,9 @@ struct ManifestFixerOptions {
// <overlay>.
std::optional<std::string> rename_overlay_target_package;
+ // The category to use instead of the one defined in 'android:category' in <overlay>.
+ std::optional<std::string> rename_overlay_category;
+
// The version name to set if 'android:versionName' is not defined in <manifest> or if
// replace_version is set.
std::optional<std::string> version_name_default;
diff --git a/tools/aapt2/link/ManifestFixer_test.cpp b/tools/aapt2/link/ManifestFixer_test.cpp
index 432f10bdab97..098d0be7f87d 100644
--- a/tools/aapt2/link/ManifestFixer_test.cpp
+++ b/tools/aapt2/link/ManifestFixer_test.cpp
@@ -351,6 +351,54 @@ TEST_F(ManifestFixerTest,
EXPECT_THAT(attr->value, StrEq("com.android"));
}
+TEST_F(ManifestFixerTest, AddOverlayCategory) {
+ ManifestFixerOptions options;
+ options.rename_overlay_category = std::string("category");
+
+ std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <overlay android:targetName="Customization" android:targetPackage="android" />
+ </manifest>)EOF",
+ options);
+ ASSERT_THAT(doc, NotNull());
+
+ xml::Element* manifest_el = doc->root.get();
+ ASSERT_THAT(manifest_el, NotNull());
+
+ xml::Element* overlay_el = manifest_el->FindChild({}, "overlay");
+ ASSERT_THAT(overlay_el, NotNull());
+
+ xml::Attribute* attr = overlay_el->FindAttribute(xml::kSchemaAndroid, "category");
+ ASSERT_THAT(attr, NotNull());
+ EXPECT_THAT(attr->value, StrEq("category"));
+}
+
+TEST_F(ManifestFixerTest, OverrideOverlayCategory) {
+ ManifestFixerOptions options;
+ options.rename_overlay_category = std::string("category");
+
+ std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <overlay android:targetName="Customization"
+ android:targetPackage="android"
+ android:category="yrogetac"/>
+ </manifest>)EOF",
+ options);
+ ASSERT_THAT(doc, NotNull());
+
+ xml::Element* manifest_el = doc->root.get();
+ ASSERT_THAT(manifest_el, NotNull());
+
+ xml::Element* overlay_el = manifest_el->FindChild({}, "overlay");
+ ASSERT_THAT(overlay_el, NotNull());
+
+ xml::Attribute* attr = overlay_el->FindAttribute(xml::kSchemaAndroid, "category");
+ ASSERT_THAT(attr, NotNull());
+ EXPECT_THAT(attr->value, StrEq("category"));
+}
+
TEST_F(ManifestFixerTest, UseDefaultVersionNameAndCode) {
ManifestFixerOptions options;
options.version_name_default = std::string("Beta");
diff --git a/tools/bit/adb.cpp b/tools/bit/adb.cpp
index f521a63255e1..201028ba900a 100644
--- a/tools/bit/adb.cpp
+++ b/tools/bit/adb.cpp
@@ -73,7 +73,7 @@ string
get_system_property(const string& name, int* err)
{
Command cmd("adb");
- cmd.AddArg("shell");
+ cmd.AddArg("exec-out");
cmd.AddArg("getprop");
cmd.AddArg(name);
@@ -278,7 +278,7 @@ run_instrumentation_test(const string& packageName, const string& runner, const
InstrumentationCallbacks* callbacks)
{
Command cmd("adb");
- cmd.AddArg("shell");
+ cmd.AddArg("exec-out");
cmd.AddArg("am");
cmd.AddArg("instrument");
cmd.AddArg("-w");
diff --git a/tools/bit/main.cpp b/tools/bit/main.cpp
index fd184f50091a..0d48070fd0c6 100644
--- a/tools/bit/main.cpp
+++ b/tools/bit/main.cpp
@@ -52,24 +52,22 @@ struct Target {
int testPassCount;
int testFailCount;
+ int testIgnoreCount;
int unknownFailureCount; // unknown failure == "Process crashed", etc.
- bool actionsWithNoTests;
Target(bool b, bool i, bool t, const string& p);
};
Target::Target(bool b, bool i, bool t, const string& p)
- :build(b),
- install(i),
- test(t),
- pattern(p),
- testActionCount(0),
- testPassCount(0),
- testFailCount(0),
- unknownFailureCount(0),
- actionsWithNoTests(false)
-{
-}
+ : build(b),
+ install(i),
+ test(t),
+ pattern(p),
+ testActionCount(0),
+ testPassCount(0),
+ testFailCount(0),
+ testIgnoreCount(0),
+ unknownFailureCount(0) {}
/**
* Command line options.
@@ -188,13 +186,12 @@ struct TestAction {
// The number of tests that failed
int failCount;
+
+ // The number of tests that were ignored (because of @Ignore)
+ int ignoreCount;
};
-TestAction::TestAction()
- :passCount(0),
- failCount(0)
-{
-}
+TestAction::TestAction() : passCount(0), failCount(0), ignoreCount(0) {}
/**
* Record for an activity that is going to be launched.
@@ -278,7 +275,7 @@ TestResults::OnTestStatus(TestStatus& status)
line << " of " << testCount;
}
}
- line << ": " << m_currentAction->target->name << ':' << className << "\\#" << testName;
+ line << ": " << m_currentAction->target->name << ':' << className << "#" << testName;
print_one_line("%s", line.str().c_str());
} else if ((resultCode == -1) || (resultCode == -2)) {
// test failed
@@ -286,9 +283,9 @@ TestResults::OnTestStatus(TestStatus& status)
// all as "failures".
m_currentAction->failCount++;
m_currentAction->target->testFailCount++;
- printf("%s\n%sFailed: %s:%s\\#%s%s\n", g_escapeClearLine, g_escapeRedBold,
- m_currentAction->target->name.c_str(), className.c_str(),
- testName.c_str(), g_escapeEndColor);
+ printf("%s\n%sFailed: %s:%s#%s%s\n", g_escapeClearLine, g_escapeRedBold,
+ m_currentAction->target->name.c_str(), className.c_str(), testName.c_str(),
+ g_escapeEndColor);
bool stackFound;
string stack = get_bundle_string(results, &stackFound, "stack", NULL);
@@ -300,6 +297,13 @@ TestResults::OnTestStatus(TestStatus& status)
} else if (stackFound) {
printf("%s\n", stack.c_str());
}
+ } else if (resultCode == -3) {
+ // test ignored
+ m_currentAction->ignoreCount++;
+ m_currentAction->target->testIgnoreCount++;
+ printf("%s\n%sIgnored: %s:%s#%s%s\n", g_escapeClearLine, g_escapeYellowBold,
+ m_currentAction->target->name.c_str(), className.c_str(), testName.c_str(),
+ g_escapeEndColor);
}
}
@@ -403,11 +407,14 @@ print_usage(FILE* out) {
fprintf(out, " Builds and installs CtsProtoTestCases.apk, and runs all the\n");
fprintf(out, " tests in the ProtoOutputStreamBoolTest class.\n");
fprintf(out, "\n");
- fprintf(out, " bit CtsProtoTestCases:.ProtoOutputStreamBoolTest\\#testWrite\n");
+ fprintf(out, " bit CtsProtoTestCases:.ProtoOutputStreamBoolTest#testWrite\n");
fprintf(out, " Builds and installs CtsProtoTestCases.apk, and runs the testWrite\n");
fprintf(out, " test method on that class.\n");
fprintf(out, "\n");
- fprintf(out, " bit CtsProtoTestCases:.ProtoOutputStreamBoolTest\\#testWrite,.ProtoOutputStreamBoolTest\\#testRepeated\n");
+ fprintf(out,
+ " bit "
+ "CtsProtoTestCases:.ProtoOutputStreamBoolTest#testWrite,.ProtoOutputStreamBoolTest#"
+ "testRepeated\n");
fprintf(out, " Builds and installs CtsProtoTestCases.apk, and runs the testWrite\n");
fprintf(out, " and testRepeated test methods on that class.\n");
fprintf(out, "\n");
@@ -450,6 +457,35 @@ print_usage(FILE* out) {
fprintf(out, "\n");
}
+/**
+ * Prints a possibly color-coded summary of test results. Example output:
+ *
+ * "34 passed, 0 failed, 1 ignored\n"
+ */
+static void print_results(int passed, int failed, int ignored) {
+ char const* nothing = "";
+ char const* cp = nothing;
+ char const* cf = nothing;
+ char const* ci = nothing;
+
+ if (failed > 0) {
+ cf = g_escapeRedBold;
+ } else if (passed > 0 || ignored > 0) {
+ cp = passed > 0 ? g_escapeGreenBold : nothing;
+ ci = ignored > 0 ? g_escapeYellowBold : nothing;
+ } else {
+ cp = g_escapeYellowBold;
+ cf = g_escapeYellowBold;
+ }
+
+ if (ignored > 0) {
+ printf("%s%d passed%s, %s%d failed%s, %s%d ignored%s\n", cp, passed, g_escapeEndColor, cf,
+ failed, g_escapeEndColor, ci, ignored, g_escapeEndColor);
+ } else {
+ printf("%s%d passed%s, %s%d failed%s\n", cp, passed, g_escapeEndColor, cf, failed,
+ g_escapeEndColor);
+ }
+}
/**
* Sets the appropriate flag* variables. If there is a problem with the
@@ -812,7 +848,7 @@ run_phases(vector<Target*> targets, const Options& options)
// Stop & Sync
if (!options.noRestart) {
- err = run_adb("shell", "stop", NULL);
+ err = run_adb("exec-out", "stop", NULL);
check_error(err);
}
err = run_adb("remount", NULL);
@@ -831,9 +867,9 @@ run_phases(vector<Target*> targets, const Options& options)
} else {
print_status("Restarting the runtime");
- err = run_adb("shell", "setprop", "sys.boot_completed", "0", NULL);
+ err = run_adb("exec-out", "setprop", "sys.boot_completed", "0", NULL);
check_error(err);
- err = run_adb("shell", "start", NULL);
+ err = run_adb("exec-out", "start", NULL);
check_error(err);
}
@@ -846,7 +882,7 @@ run_phases(vector<Target*> targets, const Options& options)
sleep(2);
}
sleep(1);
- err = run_adb("shell", "wm", "dismiss-keyguard", NULL);
+ err = run_adb("exec-out", "wm", "dismiss-keyguard", NULL);
check_error(err);
}
}
@@ -863,7 +899,7 @@ run_phases(vector<Target*> targets, const Options& options)
continue;
}
// TODO: if (!apk.file.fileInfo.exists || apk.file.HasChanged())
- err = run_adb("shell", "mkdir", "-p", dir.c_str(), NULL);
+ err = run_adb("exec-out", "mkdir", "-p", dir.c_str(), NULL);
check_error(err);
err = run_adb("push", pushed.file.filename.c_str(), pushed.dest.c_str(), NULL);
check_error(err);
@@ -945,9 +981,9 @@ run_phases(vector<Target*> targets, const Options& options)
}
}
if (runAll) {
- err = run_adb("shell", installedPath.c_str(), NULL);
+ err = run_adb("exec-out", installedPath.c_str(), NULL);
} else {
- err = run_adb("shell", installedPath.c_str(), filterArg.c_str(), NULL);
+ err = run_adb("exec-out", installedPath.c_str(), filterArg.c_str(), NULL);
}
if (err == 0) {
target->testPassCount++;
@@ -1035,22 +1071,10 @@ run_phases(vector<Target*> targets, const Options& options)
err = run_instrumentation_test(action.packageName, action.runner, action.className,
&testResults);
check_error(err);
- if (action.passCount == 0 && action.failCount == 0) {
- action.target->actionsWithNoTests = true;
- }
int total = action.passCount + action.failCount;
printf("%sRan %d test%s for %s. ", g_escapeClearLine,
total, total > 1 ? "s" : "", action.target->name.c_str());
- if (action.passCount == 0 && action.failCount == 0) {
- printf("%s%d passed, %d failed%s\n", g_escapeYellowBold, action.passCount,
- action.failCount, g_escapeEndColor);
- } else if (action.failCount > 0) {
- printf("%d passed, %s%d failed%s\n", action.passCount, g_escapeRedBold,
- action.failCount, g_escapeEndColor);
- } else {
- printf("%s%d passed%s, %d failed\n", g_escapeGreenBold, action.passCount,
- g_escapeEndColor, action.failCount);
- }
+ print_results(action.passCount, action.failCount, action.ignoreCount);
if (!testResults.IsSuccess()) {
printf("\n%sTest didn't finish successfully: %s%s\n", g_escapeRedBold,
testResults.GetErrorMessage().c_str(), g_escapeEndColor);
@@ -1073,7 +1097,7 @@ run_phases(vector<Target*> targets, const Options& options)
const ActivityAction& action = activityActions[0];
string componentName = action.packageName + "/" + action.className;
- err = run_adb("shell", "am", "start", componentName.c_str(), NULL);
+ err = run_adb("exec-out", "am", "start", componentName.c_str(), NULL);
check_error(err);
}
@@ -1147,17 +1171,11 @@ run_phases(vector<Target*> targets, const Options& options)
printf(" %sUnknown failure, see above message.%s\n",
g_escapeRedBold, g_escapeEndColor);
hasErrors = true;
- } else if (target->actionsWithNoTests) {
- printf(" %s%d passed, %d failed%s\n", g_escapeYellowBold,
- target->testPassCount, target->testFailCount, g_escapeEndColor);
- hasErrors = true;
- } else if (target->testFailCount > 0) {
- printf(" %d passed, %s%d failed%s\n", target->testPassCount,
- g_escapeRedBold, target->testFailCount, g_escapeEndColor);
- hasErrors = true;
} else {
- printf(" %s%d passed%s, %d failed\n", g_escapeGreenBold,
- target->testPassCount, g_escapeEndColor, target->testFailCount);
+ printf(" %s%s ", target->name.c_str(),
+ padding.c_str() + target->name.length());
+ print_results(target->testPassCount, target->testFailCount,
+ target->testIgnoreCount);
}
}
}
diff --git a/tools/codegen/Android.bp b/tools/codegen/Android.bp
index e53ba3e18a86..a1df878df12e 100644
--- a/tools/codegen/Android.bp
+++ b/tools/codegen/Android.bp
@@ -9,7 +9,7 @@ package {
java_binary_host {
name: "codegen_cli",
- manifest: "manifest.txt",
+ main_class: "com.android.codegen.MainKt",
srcs: [
"src/**/*.kt",
],
diff --git a/tools/codegen/manifest.txt b/tools/codegen/manifest.txt
deleted file mode 100644
index 6e1018ba6b55..000000000000
--- a/tools/codegen/manifest.txt
+++ /dev/null
@@ -1 +0,0 @@
-Main-class: com.android.codegen.MainKt
diff --git a/tools/fonts/Android.bp b/tools/fonts/Android.bp
index eeb9e3ceda1e..f8629f9bd0b8 100644
--- a/tools/fonts/Android.bp
+++ b/tools/fonts/Android.bp
@@ -24,12 +24,7 @@ package {
python_defaults {
name: "fonts_python_defaults",
version: {
- py2: {
- enabled: false,
- embedded_launcher: false,
- },
py3: {
- enabled: true,
embedded_launcher: true,
},
},
diff --git a/tools/lint/OWNERS b/tools/lint/OWNERS
index 7c0451900e32..33e237d306fc 100644
--- a/tools/lint/OWNERS
+++ b/tools/lint/OWNERS
@@ -2,4 +2,8 @@ brufino@google.com
jsharkey@google.com
per-file *CallingSettingsNonUserGetterMethods* = file:/packages/SettingsProvider/OWNERS
+per-file *RegisterReceiverFlagDetector* = jacobhobbie@google.com
+# Android lint in the Android platform maintainers
+colefaust@google.com
+farivar@google.com
diff --git a/tools/lint/README.md b/tools/lint/README.md
index b534b62cb395..b235ad60c799 100644
--- a/tools/lint/README.md
+++ b/tools/lint/README.md
@@ -1,15 +1,44 @@
-# Android Framework Lint Checker
+# Android Lint Checks for AOSP
-Custom lint checks written here are going to be executed for modules that opt in to those (e.g. any
+Custom Android Lint checks are written here to be executed against java modules
+in AOSP. These checks are broken down into two subdirectories:
+
+1. [Global Checks](#android-global-lint-checker)
+2. [Framework Checks](#android-framework-lint-checker)
+
+# [Android Global Lint Checker](/global)
+Checks written here are executed for the entire tree. The `AndroidGlobalLintChecker`
+build target produces a jar file that is included in the overall build output
+(`AndroidGlobalLintChecker.jar`). This file is then downloaded as a prebuilt under the
+`prebuilts/cmdline-tools` subproject, and included by soong with all invocations of lint.
+
+## How to add new global lint checks
+1. Write your detector with its issues and put it into
+ `global/checks/src/main/java/com/google/android/lint`.
+2. Add your detector's issues into `AndroidGlobalIssueRegistry`'s `issues`
+ field.
+3. Write unit tests for your detector in one file and put it into
+ `global/checks/test/java/com/google/android/lint`.
+4. Have your change reviewed and merged. Once your change is merged,
+ obtain a build number from a successful build that includes your change.
+5. Run `prebuilts/cmdline-tools/update-android-global-lint-checker.sh
+ <build_number>`. The script will create a commit that you can upload for
+ approval to the `prebuilts/cmdline-tools` subproject.
+6. Done! Your lint check should be applied in lint report builds across the
+ entire tree!
+
+# [Android Framework Lint Checker](/framework)
+
+Checks written here are going to be executed for modules that opt in to those (e.g. any
`services.XXX` module) and results will be automatically reported on CLs on gerrit.
-## How to add new lint checks
+## How to add new framework lint checks
1. Write your detector with its issues and put it into
- `checks/src/main/java/com/google/android/lint`.
+ `framework/checks/src/main/java/com/google/android/lint`.
2. Add your detector's issues into `AndroidFrameworkIssueRegistry`'s `issues` field.
3. Write unit tests for your detector in one file and put it into
- `checks/test/java/com/google/android/lint`.
+ `framework/checks/test/java/com/google/android/lint`.
4. Done! Your lint checks should be applied in lint report builds for modules that include
`AndroidFrameworkLintChecker`.
@@ -44,7 +73,11 @@ m out/soong/.intermediates/frameworks/base/services/autofill/services.autofill/a
environment variable with the id of the lint. For example:
`ANDROID_LINT_CHECK=UnusedTokenOfOriginalCallingIdentity m out/[...]/lint-report.html`
-## Create or update a baseline
+# How to apply automatic fixes suggested by lint
+
+See [lint_fix](fix/README.md)
+
+# Create or update a baseline
Baseline files can be used to silence known errors (and warnings) that are deemed to be safe. When
there is a lint-baseline.xml file in the root folder of the java library, soong will
@@ -75,9 +108,10 @@ locally change the soong code in
[lint.go](http://cs/aosp-master/build/soong/java/lint.go;l=451;rcl=2e778d5bc4a8d1d77b4f4a3029a4a254ad57db75)
adding `cmd.Flag("--nowarn")` and running lint again.
-## Documentation
+# Documentation
- [Android Lint Docs](https://googlesamples.github.io/android-custom-lint-rules/)
+- [Lint Check Unit Testing](https://googlesamples.github.io/android-custom-lint-rules/api-guide/unit-testing.md.html)
- [Android Lint source files](https://source.corp.google.com/studio-main/tools/base/lint/libs/lint-api/src/main/java/com/android/tools/lint/)
- [PSI source files](https://github.com/JetBrains/intellij-community/tree/master/java/java-psi-api/src/com/intellij/psi)
- [UAST source files](https://upsource.jetbrains.com/idea-ce/structure/idea-ce-7b9b8cc138bbd90aec26433f82cd2c6838694003/uast/uast-common/src/org/jetbrains/uast)
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/EnforcePermissionDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/EnforcePermissionDetector.kt
deleted file mode 100644
index 8011b36c9a8f..000000000000
--- a/tools/lint/checks/src/main/java/com/google/android/lint/EnforcePermissionDetector.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright (C) 2022 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.google.android.lint
-
-import com.android.tools.lint.detector.api.AnnotationInfo
-import com.android.tools.lint.detector.api.AnnotationOrigin
-import com.android.tools.lint.detector.api.AnnotationUsageInfo
-import com.android.tools.lint.detector.api.AnnotationUsageType
-import com.android.tools.lint.detector.api.ConstantEvaluator
-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.PsiAnnotation
-import com.intellij.psi.PsiClass
-import com.intellij.psi.PsiMethod
-import org.jetbrains.uast.UElement
-
-/**
- * Lint Detector that ensures that any method overriding a method annotated
- * with @EnforcePermission is also annotated with the exact same annotation.
- * The intent is to surface the effective permission checks to the service
- * implementations.
- */
-class EnforcePermissionDetector : Detector(), SourceCodeScanner {
-
- val ENFORCE_PERMISSION = "android.annotation.EnforcePermission"
-
- override fun applicableAnnotations(): List<String> {
- return listOf(ENFORCE_PERMISSION)
- }
-
- private fun areAnnotationsEquivalent(
- context: JavaContext,
- anno1: PsiAnnotation,
- anno2: PsiAnnotation
- ): Boolean {
- if (anno1.qualifiedName != anno2.qualifiedName) {
- return false
- }
- val attr1 = anno1.parameterList.attributes
- val attr2 = anno2.parameterList.attributes
- if (attr1.size != attr2.size) {
- return false
- }
- for (i in attr1.indices) {
- if (attr1[i].name != attr2[i].name) {
- return false
- }
- val v1 = ConstantEvaluator.evaluate(context, attr1[i].value)
- val v2 = ConstantEvaluator.evaluate(context, attr2[i].value)
- if (v1 != v2) {
- return false
- }
- }
- return true
- }
-
- override fun visitAnnotationUsage(
- context: JavaContext,
- element: UElement,
- annotationInfo: AnnotationInfo,
- usageInfo: AnnotationUsageInfo
- ) {
- if (usageInfo.type == AnnotationUsageType.EXTENDS) {
- val newClass = element.sourcePsi?.parent?.parent as PsiClass
- val extendedClass: PsiClass = usageInfo.referenced as PsiClass
- val newAnnotation = newClass.getAnnotation(ENFORCE_PERMISSION)
- val extendedAnnotation = extendedClass.getAnnotation(ENFORCE_PERMISSION)!!
-
- val location = context.getLocation(element)
- val newClassName = newClass.qualifiedName
- val extendedClassName = extendedClass.qualifiedName
- if (newAnnotation == null) {
- val msg = "The class $newClassName extends the class $extendedClassName which " +
- "is annotated with @EnforcePermission. The same annotation must be used " +
- "on $newClassName."
- context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
- } else if (!areAnnotationsEquivalent(context, newAnnotation, extendedAnnotation)) {
- val msg = "The class $newClassName is annotated with ${newAnnotation.text} " +
- "which differs from the parent class $extendedClassName: " +
- "${extendedAnnotation.text}. The same annotation must be used for " +
- "both classes."
- context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg)
- }
- } else if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE &&
- annotationInfo.origin == AnnotationOrigin.METHOD) {
- val overridingMethod = element.sourcePsi as PsiMethod
- val overriddenMethod = usageInfo.referenced as PsiMethod
- val overridingAnnotation = overridingMethod.getAnnotation(ENFORCE_PERMISSION)
- val overriddenAnnotation = overriddenMethod.getAnnotation(ENFORCE_PERMISSION)!!
-
- val location = context.getLocation(element)
- val overridingClass = overridingMethod.parent as PsiClass
- val overriddenClass = overriddenMethod.parent as PsiClass
- val overridingName = "${overridingClass.name}.${overridingMethod.name}"
- val overriddenName = "${overriddenClass.name}.${overriddenMethod.name}"
- if (overridingAnnotation == null) {
- val msg = "The method $overridingName overrides the method $overriddenName which " +
- "is annotated with @EnforcePermission. The same annotation must be used " +
- "on $overridingName"
- context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
- } else if (!areAnnotationsEquivalent(
- context, overridingAnnotation, overriddenAnnotation)) {
- val msg = "The method $overridingName is annotated with " +
- "${overridingAnnotation.text} which differs from the overridden " +
- "method $overriddenName: ${overriddenAnnotation.text}. The same " +
- "annotation must be used for both methods."
- context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg)
- }
- }
- }
-
- companion object {
- val EXPLANATION = """
- The @EnforcePermission annotation is used to indicate that the underlying binder code
- has already verified the caller's permissions before calling the appropriate method. The
- verification code is usually generated by the AIDL compiler, which also takes care of
- annotating the generated Java code.
-
- In order to surface that information to platform developers, the same annotation must be
- used on the implementation class or methods.
- """
-
- val ISSUE_MISSING_ENFORCE_PERMISSION: Issue = Issue.create(
- id = "MissingEnforcePermissionAnnotation",
- briefDescription = "Missing @EnforcePermission annotation on Binder method",
- explanation = EXPLANATION,
- category = Category.SECURITY,
- priority = 6,
- severity = Severity.ERROR,
- implementation = Implementation(
- EnforcePermissionDetector::class.java,
- Scope.JAVA_FILE_SCOPE
- )
- )
-
- val ISSUE_MISMATCHING_ENFORCE_PERMISSION: Issue = Issue.create(
- id = "MismatchingEnforcePermissionAnnotation",
- briefDescription = "Incorrect @EnforcePermission annotation on Binder method",
- explanation = EXPLANATION,
- category = Category.SECURITY,
- priority = 6,
- severity = Severity.ERROR,
- implementation = Implementation(
- EnforcePermissionDetector::class.java,
- Scope.JAVA_FILE_SCOPE
- )
- )
- }
-}
diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/EnforcePermissionDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/EnforcePermissionDetectorTest.kt
deleted file mode 100644
index f5f4ebee24e0..000000000000
--- a/tools/lint/checks/src/test/java/com/google/android/lint/EnforcePermissionDetectorTest.kt
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2022 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.google.android.lint
-
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFile
-import com.android.tools.lint.checks.infrastructure.TestLintTask
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Issue
-
-@Suppress("UnstableApiUsage")
-class EnforcePermissionDetectorTest : LintDetectorTest() {
- override fun getDetector(): Detector = EnforcePermissionDetector()
-
- override fun getIssues(): List<Issue> = listOf(
- EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION,
- EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION
- )
-
- override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
-
- fun testDoesNotDetectIssuesCorrectAnnotationOnClass() {
- lint().files(java(
- """
- package test.pkg;
- @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
- public class TestClass1 extends IFoo.Stub {
- public void testMethod() {}
- }
- """).indented(),
- *stubs
- )
- .run()
- .expectClean()
- }
-
- fun testDoesNotDetectIssuesCorrectAnnotationOnMethod() {
- lint().files(java(
- """
- package test.pkg;
- import android.annotation.EnforcePermission;
- public class TestClass2 extends IFooMethod.Stub {
- @Override
- @EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
- public void testMethod() {}
- }
- """).indented(),
- *stubs
- )
- .run()
- .expectClean()
- }
-
- fun testDetectIssuesMismatchingAnnotationOnClass() {
- lint().files(java(
- """
- package test.pkg;
- @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET)
- public class TestClass3 extends IFoo.Stub {
- public void testMethod() {}
- }
- """).indented(),
- *stubs
- )
- .run()
- .expect("""src/test/pkg/TestClass3.java:3: Error: The class test.pkg.TestClass3 is \
-annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \
-which differs from the parent class IFoo.Stub: \
-@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The \
-same annotation must be used for both classes. [MismatchingEnforcePermissionAnnotation]
-public class TestClass3 extends IFoo.Stub {
- ~~~~~~~~~
-1 errors, 0 warnings""".addLineContinuation())
- }
-
- fun testDetectIssuesMismatchingAnnotationOnMethod() {
- lint().files(java(
- """
- package test.pkg;
- public class TestClass4 extends IFooMethod.Stub {
- @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET)
- public void testMethod() {}
- }
- """).indented(),
- *stubs
- )
- .run()
- .expect("""src/test/pkg/TestClass4.java:4: Error: The method TestClass4.testMethod is \
-annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \
-which differs from the overridden method Stub.testMethod: \
-@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The same \
-annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
- public void testMethod() {}
- ~~~~~~~~~~
-1 errors, 0 warnings""".addLineContinuation())
- }
-
- fun testDetectIssuesMissingAnnotationOnClass() {
- lint().files(java(
- """
- package test.pkg;
- public class TestClass5 extends IFoo.Stub {
- public void testMethod() {}
- }
- """).indented(),
- *stubs
- )
- .run()
- .expect("""src/test/pkg/TestClass5.java:2: Error: The class test.pkg.TestClass5 extends \
-the class IFoo.Stub which is annotated with @EnforcePermission. The same annotation must be \
-used on test.pkg.TestClass5. [MissingEnforcePermissionAnnotation]
-public class TestClass5 extends IFoo.Stub {
- ~~~~~~~~~
-1 errors, 0 warnings""".addLineContinuation())
- }
-
- fun testDetectIssuesMissingAnnotationOnMethod() {
- lint().files(java(
- """
- package test.pkg;
- public class TestClass6 extends IFooMethod.Stub {
- public void testMethod() {}
- }
- """).indented(),
- *stubs
- )
- .run()
- .expect("""src/test/pkg/TestClass6.java:3: Error: The method TestClass6.testMethod \
-overrides the method Stub.testMethod which is annotated with @EnforcePermission. The same \
-annotation must be used on TestClass6.testMethod [MissingEnforcePermissionAnnotation]
- public void testMethod() {}
- ~~~~~~~~~~
-1 errors, 0 warnings""".addLineContinuation())
- }
-
- /* Stubs */
-
- private val interfaceIFooStub: TestFile = java(
- """
- @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
- public interface IFoo {
- @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
- public static abstract class Stub implements IFoo {
- @Override
- public void testMethod() {}
- }
- public void testMethod();
- }
- """
- ).indented()
-
- private val interfaceIFooMethodStub: TestFile = java(
- """
- public interface IFooMethod {
- public static abstract class Stub implements IFooMethod {
- @Override
- @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
- public void testMethod() {}
- }
- @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
- public void testMethod();
- }
- """
- ).indented()
-
- private val manifestPermissionStub: TestFile = java(
- """
- package android.Manifest;
- class permission {
- public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
- public static final String INTERNET = "android.permission.INTERNET";
- }
- """
- ).indented()
-
- private val enforcePermissionAnnotationStub: TestFile = java(
- """
- package android.annotation;
- public @interface EnforcePermission {}
- """
- ).indented()
-
- private val stubs = arrayOf(interfaceIFooStub, interfaceIFooMethodStub,
- manifestPermissionStub, enforcePermissionAnnotationStub)
-
- // Substitutes "backslash + new line" with an empty string to imitate line continuation
- private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
-}
diff --git a/tools/lint/common/Android.bp b/tools/lint/common/Android.bp
new file mode 100644
index 000000000000..898f88b8759c
--- /dev/null
+++ b/tools/lint/common/Android.bp
@@ -0,0 +1,29 @@
+// Copyright (C) 2022 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library_host {
+ name: "AndroidCommonLint",
+ srcs: ["src/main/java/**/*.kt"],
+ libs: ["lint_api"],
+ kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt b/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt
new file mode 100644
index 000000000000..0ef165f1523b
--- /dev/null
+++ b/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint
+
+import com.google.android.lint.model.Method
+
+const val CLASS_STUB = "Stub"
+const val CLASS_CONTEXT = "android.content.Context"
+const val CLASS_ACTIVITY_MANAGER_SERVICE = "com.android.server.am.ActivityManagerService"
+const val CLASS_ACTIVITY_MANAGER_INTERNAL = "android.app.ActivityManagerInternal"
+
+// Enforce permission APIs
+val ENFORCE_PERMISSION_METHODS = listOf(
+ Method(CLASS_CONTEXT, "checkPermission"),
+ Method(CLASS_CONTEXT, "checkCallingPermission"),
+ Method(CLASS_CONTEXT, "checkCallingOrSelfPermission"),
+ Method(CLASS_CONTEXT, "enforcePermission"),
+ Method(CLASS_CONTEXT, "enforceCallingPermission"),
+ Method(CLASS_CONTEXT, "enforceCallingOrSelfPermission"),
+ Method(CLASS_ACTIVITY_MANAGER_SERVICE, "checkPermission"),
+ Method(CLASS_ACTIVITY_MANAGER_INTERNAL, "enforceCallingPermission")
+)
+
+const val ANNOTATION_PERMISSION_METHOD = "android.annotation.PermissionMethod"
+const val ANNOTATION_PERMISSION_NAME = "android.annotation.PermissionName"
+const val ANNOTATION_PERMISSION_RESULT = "android.content.pm.PackageManager.PermissionResult"
diff --git a/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt b/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt
new file mode 100644
index 000000000000..9a7f8fa53d87
--- /dev/null
+++ b/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint
+
+import com.android.tools.lint.detector.api.getUMethod
+import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UParameter
+import org.jetbrains.uast.UQualifiedReferenceExpression
+
+fun isPermissionMethodCall(callExpression: UCallExpression): Boolean {
+ val method = callExpression.resolve()?.getUMethod() ?: return false
+ return hasPermissionMethodAnnotation(method)
+}
+
+fun hasPermissionMethodAnnotation(method: UMethod): Boolean =
+ getPermissionMethodAnnotation(method) != null
+
+fun getPermissionMethodAnnotation(method: UMethod?): UAnnotation? = method?.uAnnotations
+ ?.firstOrNull { it.qualifiedName == ANNOTATION_PERMISSION_METHOD }
+
+fun hasPermissionNameAnnotation(parameter: UParameter) = parameter.annotations.any {
+ it.hasQualifiedName(ANNOTATION_PERMISSION_NAME)
+}
+
+/**
+ * Attempts to return a CallExpression from a QualifiedReferenceExpression (or returns it directly if passed directly)
+ * @param callOrReferenceCall expected to be UCallExpression or UQualifiedReferenceExpression
+ * @return UCallExpression, if available
+ */
+fun findCallExpression(callOrReferenceCall: UElement?): UCallExpression? =
+ when (callOrReferenceCall) {
+ is UCallExpression -> callOrReferenceCall
+ is UQualifiedReferenceExpression -> callOrReferenceCall.selector as? UCallExpression
+ else -> null
+ }
diff --git a/tools/lint/common/src/main/java/com/google/android/lint/model/Method.kt b/tools/lint/common/src/main/java/com/google/android/lint/model/Method.kt
new file mode 100644
index 000000000000..3939b6109eaa
--- /dev/null
+++ b/tools/lint/common/src/main/java/com/google/android/lint/model/Method.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.model
+
+/**
+ * Data class to represent a Method
+ */
+data class Method(val clazz: String, val name: String) {
+ override fun toString(): String {
+ return "$clazz#$name"
+ }
+}
diff --git a/tools/lint/fix/Android.bp b/tools/lint/fix/Android.bp
new file mode 100644
index 000000000000..43f21221ae5a
--- /dev/null
+++ b/tools/lint/fix/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2022 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+python_binary_host {
+ name: "lint_fix",
+ main: "soong_lint_fix.py",
+ srcs: ["soong_lint_fix.py"],
+}
+
+python_library_host {
+ name: "soong_lint_fix",
+ srcs: ["soong_lint_fix.py"],
+}
diff --git a/tools/lint/fix/README.md b/tools/lint/fix/README.md
new file mode 100644
index 000000000000..a5ac2be1c18a
--- /dev/null
+++ b/tools/lint/fix/README.md
@@ -0,0 +1,30 @@
+# Refactoring the platform with lint
+Inspiration: go/refactor-the-platform-with-lint\
+**Special Thanks: brufino@, azharaa@, for the prior work that made this all possible**
+
+## What is this?
+
+It's a python script that runs the framework linter,
+and then (optionally) copies modified files back into the source tree.\
+Why python, you ask? Because python is cool ¯\_(ツ)_/¯.
+
+Incidentally, this exposes a much simpler way to run individual lint checks
+against individual modules, so it's useful beyond applying fixes.
+
+## Why?
+
+Lint is not allowed to modify source files directly via lint's `--apply-suggestions` flag.
+As a compromise, soong zips up the (potentially) modified sources and leaves them in an intermediate
+directory. This script runs the lint, unpacks those files, and copies them back into the tree.
+
+## How do I run it?
+**WARNING: You probably want to commit/stash any changes to your working tree before doing this...**
+
+```
+source build/envsetup.sh
+lunch cf_x86_64_phone-userdebug # or any lunch target
+m lint_fix
+lint_fix -h
+```
+
+The script's help output explains things that are omitted here.
diff --git a/tools/lint/fix/soong_lint_fix.py b/tools/lint/fix/soong_lint_fix.py
new file mode 100644
index 000000000000..cd4d778d1dec
--- /dev/null
+++ b/tools/lint/fix/soong_lint_fix.py
@@ -0,0 +1,173 @@
+# Copyright (C) 2022 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.
+
+import argparse
+import json
+import os
+import shutil
+import subprocess
+import sys
+import zipfile
+
+ANDROID_BUILD_TOP = os.environ.get("ANDROID_BUILD_TOP")
+ANDROID_PRODUCT_OUT = os.environ.get("ANDROID_PRODUCT_OUT")
+PRODUCT_OUT = ANDROID_PRODUCT_OUT.removeprefix(f"{ANDROID_BUILD_TOP}/")
+
+SOONG_UI = "build/soong/soong_ui.bash"
+PATH_PREFIX = "out/soong/.intermediates"
+PATH_SUFFIX = "android_common/lint"
+FIX_ZIP = "suggested-fixes.zip"
+
+class SoongLintFix:
+ """
+ This class creates a command line tool that will
+ apply lint fixes to the platform via the necessary
+ combination of soong and shell commands.
+
+ It breaks up these operations into a few "private" methods
+ that are intentionally exposed so experimental code can tweak behavior.
+
+ The entry point, `run`, will apply lint fixes using the
+ intermediate `suggested-fixes` directory that soong creates during its
+ invocation of lint.
+
+ Basic usage:
+ ```
+ from soong_lint_fix import SoongLintFix
+
+ SoongLintFix().run()
+ ```
+ """
+ def __init__(self):
+ self._parser = _setup_parser()
+ self._args = None
+ self._kwargs = None
+ self._path = None
+ self._target = None
+
+
+ def run(self, additional_setup=None, custom_fix=None):
+ """
+ Run the script
+ """
+ self._setup()
+ self._find_module()
+ self._lint()
+
+ if not self._args.no_fix:
+ self._fix()
+
+ if self._args.print:
+ self._print()
+
+ def _setup(self):
+ self._args = self._parser.parse_args()
+ env = os.environ.copy()
+ if self._args.check:
+ env["ANDROID_LINT_CHECK"] = self._args.check
+ if self._args.lint_module:
+ env["ANDROID_LINT_CHECK_EXTRA_MODULES"] = self._args.lint_module
+
+ self._kwargs = {
+ "env": env,
+ "executable": "/bin/bash",
+ "shell": True,
+ }
+
+ os.chdir(ANDROID_BUILD_TOP)
+
+
+ def _find_module(self):
+ print("Refreshing soong modules...")
+ try:
+ os.mkdir(ANDROID_PRODUCT_OUT)
+ except OSError:
+ pass
+ subprocess.call(f"{SOONG_UI} --make-mode {PRODUCT_OUT}/module-info.json", **self._kwargs)
+ print("done.")
+
+ with open(f"{ANDROID_PRODUCT_OUT}/module-info.json") as f:
+ module_info = json.load(f)
+
+ if self._args.module not in module_info:
+ sys.exit(f"Module {self._args.module} not found!")
+
+ module_path = module_info[self._args.module]["path"][0]
+ print(f"Found module {module_path}/{self._args.module}.")
+
+ self._path = f"{PATH_PREFIX}/{module_path}/{self._args.module}/{PATH_SUFFIX}"
+ self._target = f"{self._path}/lint-report.txt"
+
+
+ def _lint(self):
+ print("Cleaning up any old lint results...")
+ try:
+ os.remove(f"{self._target}")
+ os.remove(f"{self._path}/{FIX_ZIP}")
+ except FileNotFoundError:
+ pass
+ print("done.")
+
+ print(f"Generating {self._target}")
+ subprocess.call(f"{SOONG_UI} --make-mode {self._target}", **self._kwargs)
+ print("done.")
+
+
+ def _fix(self):
+ print("Copying suggested fixes to the tree...")
+ with zipfile.ZipFile(f"{self._path}/{FIX_ZIP}") as zip:
+ for name in zip.namelist():
+ if name.startswith("out") or not name.endswith(".java"):
+ continue
+ with zip.open(name) as src, open(f"{ANDROID_BUILD_TOP}/{name}", "wb") as dst:
+ shutil.copyfileobj(src, dst)
+ print("done.")
+
+
+ def _print(self):
+ print("### lint-report.txt ###", end="\n\n")
+ with open(self._target, "r") as f:
+ print(f.read())
+
+
+def _setup_parser():
+ parser = argparse.ArgumentParser(description="""
+ This is a python script that applies lint fixes to the platform:
+ 1. Set up the environment, etc.
+ 2. Run lint on the specified target.
+ 3. Copy the modified files, from soong's intermediate directory, back into the tree.
+
+ **Gotcha**: You must have run `source build/envsetup.sh` and `lunch` first.
+ """, formatter_class=argparse.RawTextHelpFormatter)
+
+ parser.add_argument('module',
+ help='The soong build module to run '
+ '(e.g. framework-minus-apex or services.core.unboosted)')
+
+ parser.add_argument('--check',
+ help='Which lint to run. Passed to the ANDROID_LINT_CHECK environment variable.')
+
+ parser.add_argument('--lint-module',
+ help='Specific lint module to run. Passed to the ANDROID_LINT_CHECK_EXTRA_MODULES environment variable.')
+
+ parser.add_argument('--no-fix', action='store_true',
+ help='Just build and run the lint, do NOT apply the fixes.')
+
+ parser.add_argument('--print', action='store_true',
+ help='Print the contents of the generated lint-report.txt at the end.')
+
+ return parser
+
+if __name__ == "__main__":
+ SoongLintFix().run() \ No newline at end of file
diff --git a/tools/lint/Android.bp b/tools/lint/framework/Android.bp
index 17547ef8b561..30a6daaef2a4 100644
--- a/tools/lint/Android.bp
+++ b/tools/lint/framework/Android.bp
@@ -1,4 +1,4 @@
-// Copyright (C) 2021 The Android Open Source Project
+// Copyright (C) 2022 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.
@@ -29,6 +29,10 @@ java_library_host {
"auto_service_annotations",
"lint_api",
],
+ static_libs: [
+ "AndroidCommonLint",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
}
java_test_host {
@@ -42,5 +46,19 @@ java_test_host {
],
test_options: {
unit_test: true,
+ tradefed_options: [
+ {
+ // lint bundles in some classes that were built with older versions
+ // of libraries, and no longer load. Since tradefed tries to load
+ // all classes in the jar to look for tests, it crashes loading them.
+ // Exclude these classes from tradefed's search.
+ name: "exclude-paths",
+ value: "org/apache",
+ },
+ {
+ name: "exclude-paths",
+ value: "META-INF",
+ },
+ ],
},
}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
new file mode 100644
index 000000000000..935badecf8d5
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 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.google.android.lint
+
+import com.android.tools.lint.client.api.IssueRegistry
+import com.android.tools.lint.client.api.Vendor
+import com.android.tools.lint.detector.api.CURRENT_API
+import com.google.android.lint.parcel.SaferParcelChecker
+import com.google.auto.service.AutoService
+
+@AutoService(IssueRegistry::class)
+@Suppress("UnstableApiUsage")
+class AndroidFrameworkIssueRegistry : IssueRegistry() {
+ override val issues = listOf(
+ CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN,
+ CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN,
+ CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
+ CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
+ CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
+ CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
+ CallingIdentityTokenDetector.ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE,
+ CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED,
+ SaferParcelChecker.ISSUE_UNSAFE_API_USAGE,
+ // TODO: Currently crashes due to OOM issue
+ // PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
+ PermissionMethodDetector.ISSUE_PERMISSION_METHOD_USAGE,
+ PermissionMethodDetector.ISSUE_CAN_BE_PERMISSION_METHOD,
+ )
+
+ override val api: Int
+ get() = CURRENT_API
+
+ override val minApi: Int
+ get() = 8
+
+ override val vendor: Vendor = Vendor(
+ vendorName = "Android",
+ feedbackUrl = "http://b/issues/new?component=315013",
+ contact = "brufino@google.com"
+ )
+}
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt
index 930378b168b2..0c375c358e61 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt
@@ -33,6 +33,7 @@ import org.jetbrains.uast.UBlockExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UDeclarationsExpression
import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UIfExpression
import org.jetbrains.uast.ULocalVariable
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.UTryExpression
@@ -52,10 +53,10 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
private val tokensMap = mutableMapOf<String, Token>()
override fun getApplicableUastTypes(): List<Class<out UElement?>> =
- listOf(ULocalVariable::class.java, UCallExpression::class.java)
+ listOf(ULocalVariable::class.java, UCallExpression::class.java)
override fun createUastHandler(context: JavaContext): UElementHandler =
- TokenUastHandler(context)
+ TokenUastHandler(context)
/** File analysis starts with a clear map */
override fun beforeCheckFile(context: Context) {
@@ -70,9 +71,9 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
override fun afterCheckFile(context: Context) {
for (token in tokensMap.values) {
context.report(
- ISSUE_UNUSED_TOKEN,
- token.location,
- getIncidentMessageUnusedToken(token.variableName)
+ ISSUE_UNUSED_TOKEN,
+ token.location,
+ getIncidentMessageUnusedToken(token.variableName)
)
}
tokensMap.clear()
@@ -96,9 +97,9 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
val variableName = node.getName()
if (!node.isFinal) {
context.report(
- ISSUE_NON_FINAL_TOKEN,
- location,
- getIncidentMessageNonFinalToken(variableName)
+ ISSUE_NON_FINAL_TOKEN,
+ location,
+ getIncidentMessageNonFinalToken(variableName)
)
}
// If there exists an unused variable with the same name in the map, we can imply that
@@ -106,9 +107,9 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
val oldToken = tokensMap[variableName]
if (oldToken != null) {
context.report(
- ISSUE_UNUSED_TOKEN,
- oldToken.location,
- getIncidentMessageUnusedToken(oldToken.variableName)
+ ISSUE_UNUSED_TOKEN,
+ oldToken.location,
+ getIncidentMessageUnusedToken(oldToken.variableName)
)
}
// If there exists a token in the same scope as the current new token, it means that
@@ -117,56 +118,84 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
val firstCallToken = findFirstTokenInScope(node)
if (firstCallToken != null) {
context.report(
- ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
- createNestedLocation(firstCallToken, location),
- getIncidentMessageNestedClearIdentityCallsPrimary(
- firstCallToken.variableName,
- variableName
- )
+ ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
+ createNestedLocation(firstCallToken, location),
+ getIncidentMessageNestedClearIdentityCallsPrimary(
+ firstCallToken.variableName,
+ variableName
+ )
)
}
// If the next statement in the tree is not a try-finally statement, we need to report
// the "clearCallingIdentity() is not followed by try-finally" issue
val finallyClause = (getNextStatementOfLocalVariable(node) as? UTryExpression)
- ?.finallyClause
+ ?.finallyClause
if (finallyClause == null) {
context.report(
- ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
- location,
- getIncidentMessageClearIdentityCallNotFollowedByTryFinally(variableName)
+ ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
+ location,
+ getIncidentMessageClearIdentityCallNotFollowedByTryFinally(variableName)
)
}
tokensMap[variableName] = Token(
- variableName,
- node.sourcePsi?.getUseScope(),
- location,
- finallyClause
+ variableName,
+ node.sourcePsi?.getUseScope(),
+ location,
+ finallyClause
)
}
- /**
- * For every method():
- * - Checks use of caller-aware methods issue
- * For every call of Binder.restoreCallingIdentity(token):
- * - Checks for restoreCallingIdentity() not in the finally block issue
- * - Removes token from tokensMap if token is within the scope of the method
- */
override fun visitCallExpression(node: UCallExpression) {
+ when {
+ isMethodCall(node, Method.BINDER_CLEAR_CALLING_IDENTITY) -> {
+ checkClearCallingIdentityCall(node)
+ }
+ isMethodCall(node, Method.BINDER_RESTORE_CALLING_IDENTITY) -> {
+ checkRestoreCallingIdentityCall(node)
+ }
+ isCallerAwareMethod(node) -> checkCallerAwareMethod(node)
+ }
+ }
+
+ private fun checkClearCallingIdentityCall(node: UCallExpression) {
+ var firstNonQualifiedParent = getFirstNonQualifiedParent(node)
+ // if the call expression is inside a ternary, and the ternary is assigned
+ // to a variable, then we are still technically assigning
+ // any result of clearCallingIdentity to a variable
+ if (firstNonQualifiedParent is UIfExpression && firstNonQualifiedParent.isTernary) {
+ firstNonQualifiedParent = firstNonQualifiedParent.uastParent
+ }
+ if (firstNonQualifiedParent !is ULocalVariable) {
+ context.report(
+ ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE,
+ context.getLocation(node),
+ getIncidentMessageResultOfClearIdentityCallNotStoredInVariable(
+ node.getQualifiedParentOrThis().asRenderString()
+ )
+ )
+ }
+ }
+
+ private fun checkCallerAwareMethod(node: UCallExpression) {
val token = findFirstTokenInScope(node)
- if (isCallerAwareMethod(node) && token != null) {
+ if (token != null) {
context.report(
- ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
- context.getLocation(node),
- getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity(
- token.variableName,
- node.asRenderString()
- )
+ ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
+ context.getLocation(node),
+ getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity(
+ token.variableName,
+ node.asRenderString()
+ )
)
- return
}
- if (!isMethodCall(node, Method.BINDER_RESTORE_CALLING_IDENTITY)) return
- val first = node.valueArguments[0].skipParenthesizedExprDown()
- val arg = first as? USimpleNameReferenceExpression ?: return
+ }
+
+ /**
+ * - Checks for restoreCallingIdentity() not in the finally block issue
+ * - Removes token from tokensMap if token is within the scope of the method
+ */
+ private fun checkRestoreCallingIdentityCall(node: UCallExpression) {
+ val arg = node.valueArguments[0] as? USimpleNameReferenceExpression ?: return
val variableName = arg.identifier
val originalScope = tokensMap[variableName]?.scope ?: return
val psi = arg.sourcePsi ?: return
@@ -174,26 +203,31 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
// token declaration. If not within the scope, no action is needed because the token is
// irrelevant i.e. not in the same scope or was not declared with clearCallingIdentity()
if (!PsiSearchScopeUtil.isInScope(originalScope, psi)) return
- // - We do not report "restore identity call not in finally" issue when there is no
+ // We do not report "restore identity call not in finally" issue when there is no
// finally block because that case is already handled by "clear identity call not
// followed by try-finally" issue
- // - UCallExpression can be a child of UQualifiedReferenceExpression, i.e.
- // receiver.selector, so to get the call's immediate parent we need to get the topmost
- // parent qualified reference expression and access its parent
if (tokensMap[variableName]?.finallyBlock != null &&
- skipParenthesizedExprUp(node.getQualifiedParentOrThis().uastParent) !=
- tokensMap[variableName]?.finallyBlock) {
+ getFirstNonQualifiedParent(node) !=
+ tokensMap[variableName]?.finallyBlock
+ ) {
context.report(
- ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
- context.getLocation(node),
- getIncidentMessageRestoreIdentityCallNotInFinallyBlock(variableName)
+ ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
+ context.getLocation(node),
+ getIncidentMessageRestoreIdentityCallNotInFinallyBlock(variableName)
)
}
tokensMap.remove(variableName)
}
+ private fun getFirstNonQualifiedParent(expression: UCallExpression): UElement? {
+ // UCallExpression can be a child of UQualifiedReferenceExpression, i.e.
+ // receiver.selector, so to get the call's immediate parent we need to get the topmost
+ // parent qualified reference expression and access its parent
+ return skipParenthesizedExprUp(expression.getQualifiedParentOrThis().uastParent)
+ }
+
private fun isCallerAwareMethod(expression: UCallExpression): Boolean =
- callerAwareMethods.any { method -> isMethodCall(expression, method) }
+ callerAwareMethods.any { method -> isMethodCall(expression, method) }
private fun isMethodCall(
expression: UCallExpression,
@@ -201,12 +235,12 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
): Boolean {
val psiMethod = expression.resolve() ?: return false
return psiMethod.getName() == method.methodName &&
- context.evaluator.methodMatches(
- psiMethod,
- method.className,
- /* allowInherit */ true,
- *method.args
- )
+ context.evaluator.methodMatches(
+ psiMethod,
+ method.className,
+ /* allowInherit */ true,
+ *method.args
+ )
}
/**
@@ -255,7 +289,7 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
return declarations[indexInDeclarations + 1]
}
val enclosingBlock = node
- .getParentOfType<UBlockExpression>(strict = true) ?: return null
+ .getParentOfType<UBlockExpression>(strict = true) ?: return null
val expressions = enclosingBlock.expressions
val indexInBlock = expressions.indexOf(declarationsExpression as UElement)
return if (indexInBlock == -1) null else expressions.getOrNull(indexInBlock + 1)
@@ -301,12 +335,12 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
secondCallTokenLocation: Location
): Location {
return cloneLocation(secondCallTokenLocation)
- .withSecondary(
- cloneLocation(firstCallToken.location),
- getIncidentMessageNestedClearIdentityCallsSecondary(
- firstCallToken.variableName
- )
+ .withSecondary(
+ cloneLocation(firstCallToken.location),
+ getIncidentMessageNestedClearIdentityCallsSecondary(
+ firstCallToken.variableName
)
+ )
}
private fun cloneLocation(location: Location): Location {
@@ -347,20 +381,20 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
const val CLASS_USER_HANDLE = "android.os.UserHandle"
private val callerAwareMethods = listOf(
- Method.BINDER_GET_CALLING_PID,
- Method.BINDER_GET_CALLING_UID,
- Method.BINDER_GET_CALLING_UID_OR_THROW,
- Method.BINDER_GET_CALLING_USER_HANDLE,
- Method.USER_HANDLE_GET_CALLING_APP_ID,
- Method.USER_HANDLE_GET_CALLING_USER_ID
+ Method.BINDER_GET_CALLING_PID,
+ Method.BINDER_GET_CALLING_UID,
+ Method.BINDER_GET_CALLING_UID_OR_THROW,
+ Method.BINDER_GET_CALLING_USER_HANDLE,
+ Method.USER_HANDLE_GET_CALLING_APP_ID,
+ Method.USER_HANDLE_GET_CALLING_USER_ID
)
/** Issue: unused token from Binder.clearCallingIdentity() */
@JvmField
val ISSUE_UNUSED_TOKEN: Issue = Issue.create(
- id = "UnusedTokenOfOriginalCallingIdentity",
- briefDescription = "Unused token of Binder.clearCallingIdentity()",
- explanation = """
+ id = "UnusedTokenOfOriginalCallingIdentity",
+ briefDescription = "Unused token of Binder.clearCallingIdentity()",
+ explanation = """
You cleared the original calling identity with \
`Binder.clearCallingIdentity()`, but have not used the returned token to \
restore the identity.
@@ -370,26 +404,26 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
`token` is the result of `Binder.clearCallingIdentity()`
""",
- category = Category.SECURITY,
- priority = 6,
- severity = Severity.WARNING,
- implementation = Implementation(
- CallingIdentityTokenDetector::class.java,
- Scope.JAVA_FILE_SCOPE
- )
+ category = Category.SECURITY,
+ priority = 6,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ CallingIdentityTokenDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
)
private fun getIncidentMessageUnusedToken(variableName: String) = "`$variableName` has " +
- "not been used to restore the calling identity. Introduce a `try`-`finally` " +
- "after the declaration and call `Binder.restoreCallingIdentity($variableName)` " +
- "in `finally` or remove `$variableName`."
+ "not been used to restore the calling identity. Introduce a `try`-`finally` " +
+ "after the declaration and call `Binder.restoreCallingIdentity($variableName)` " +
+ "in `finally` or remove `$variableName`."
/** Issue: non-final token from Binder.clearCallingIdentity() */
@JvmField
val ISSUE_NON_FINAL_TOKEN: Issue = Issue.create(
- id = "NonFinalTokenOfOriginalCallingIdentity",
- briefDescription = "Non-final token of Binder.clearCallingIdentity()",
- explanation = """
+ id = "NonFinalTokenOfOriginalCallingIdentity",
+ briefDescription = "Non-final token of Binder.clearCallingIdentity()",
+ explanation = """
You cleared the original calling identity with \
`Binder.clearCallingIdentity()`, but have not made the returned token `final`.
@@ -397,47 +431,47 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
which can cause problems when restoring the identity with \
`Binder.restoreCallingIdentity(token)`.
""",
- category = Category.SECURITY,
- priority = 6,
- severity = Severity.WARNING,
- implementation = Implementation(
- CallingIdentityTokenDetector::class.java,
- Scope.JAVA_FILE_SCOPE
- )
+ category = Category.SECURITY,
+ priority = 6,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ CallingIdentityTokenDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
)
private fun getIncidentMessageNonFinalToken(variableName: String) = "`$variableName` is " +
- "a non-final token from `Binder.clearCallingIdentity()`. Add `final` keyword to " +
- "`$variableName`."
+ "a non-final token from `Binder.clearCallingIdentity()`. Add `final` keyword to " +
+ "`$variableName`."
/** Issue: nested calls of Binder.clearCallingIdentity() */
@JvmField
val ISSUE_NESTED_CLEAR_IDENTITY_CALLS: Issue = Issue.create(
- id = "NestedClearCallingIdentityCalls",
- briefDescription = "Nested calls of Binder.clearCallingIdentity()",
- explanation = """
+ id = "NestedClearCallingIdentityCalls",
+ briefDescription = "Nested calls of Binder.clearCallingIdentity()",
+ explanation = """
You cleared the original calling identity with \
`Binder.clearCallingIdentity()` twice without restoring identity with the \
result of the first call.
Make sure to restore the identity after each clear identity call.
""",
- category = Category.SECURITY,
- priority = 6,
- severity = Severity.WARNING,
- implementation = Implementation(
- CallingIdentityTokenDetector::class.java,
- Scope.JAVA_FILE_SCOPE
- )
+ category = Category.SECURITY,
+ priority = 6,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ CallingIdentityTokenDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
)
private fun getIncidentMessageNestedClearIdentityCallsPrimary(
firstCallVariableName: String,
secondCallVariableName: String
): String = "The calling identity has already been cleared and returned into " +
- "`$firstCallVariableName`. Move `$secondCallVariableName` declaration after " +
- "restoring the calling identity with " +
- "`Binder.restoreCallingIdentity($firstCallVariableName)`."
+ "`$firstCallVariableName`. Move `$secondCallVariableName` declaration after " +
+ "restoring the calling identity with " +
+ "`Binder.restoreCallingIdentity($firstCallVariableName)`."
private fun getIncidentMessageNestedClearIdentityCallsSecondary(
firstCallVariableName: String
@@ -446,10 +480,10 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
/** Issue: Binder.clearCallingIdentity() is not followed by `try-finally` statement */
@JvmField
val ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY: Issue = Issue.create(
- id = "ClearIdentityCallNotFollowedByTryFinally",
- briefDescription = "Binder.clearCallingIdentity() is not followed by try-finally " +
- "statement",
- explanation = """
+ id = "ClearIdentityCallNotFollowedByTryFinally",
+ briefDescription = "Binder.clearCallingIdentity() is not followed by try-finally " +
+ "statement",
+ explanation = """
You cleared the original calling identity with \
`Binder.clearCallingIdentity()`, but the next statement is not a `try` \
statement.
@@ -472,30 +506,30 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
code with your identity that was originally intended to run with the calling \
application's identity.
""",
- category = Category.SECURITY,
- priority = 6,
- severity = Severity.WARNING,
- implementation = Implementation(
- CallingIdentityTokenDetector::class.java,
- Scope.JAVA_FILE_SCOPE
- )
+ category = Category.SECURITY,
+ priority = 6,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ CallingIdentityTokenDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
)
private fun getIncidentMessageClearIdentityCallNotFollowedByTryFinally(
variableName: String
): String = "You cleared the calling identity and returned the result into " +
- "`$variableName`, but the next statement is not a `try`-`finally` statement. " +
- "Define a `try`-`finally` block after `$variableName` declaration to ensure a " +
- "safe restore of the calling identity by calling " +
- "`Binder.restoreCallingIdentity($variableName)` and making it an immediate child " +
- "of the `finally` block."
+ "`$variableName`, but the next statement is not a `try`-`finally` statement. " +
+ "Define a `try`-`finally` block after `$variableName` declaration to ensure a " +
+ "safe restore of the calling identity by calling " +
+ "`Binder.restoreCallingIdentity($variableName)` and making it an immediate child " +
+ "of the `finally` block."
/** Issue: Binder.restoreCallingIdentity() is not in finally block */
@JvmField
val ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK: Issue = Issue.create(
- id = "RestoreIdentityCallNotInFinallyBlock",
- briefDescription = "Binder.restoreCallingIdentity() is not in finally block",
- explanation = """
+ id = "RestoreIdentityCallNotInFinallyBlock",
+ briefDescription = "Binder.restoreCallingIdentity() is not in finally block",
+ explanation = """
You are restoring the original calling identity with \
`Binder.restoreCallingIdentity()`, but the call is not an immediate child of \
the `finally` block of the `try` statement.
@@ -516,28 +550,28 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
`finally` block, you may run code with your identity that was originally \
intended to run with the calling application's identity.
""",
- category = Category.SECURITY,
- priority = 6,
- severity = Severity.WARNING,
- implementation = Implementation(
- CallingIdentityTokenDetector::class.java,
- Scope.JAVA_FILE_SCOPE
- )
+ category = Category.SECURITY,
+ priority = 6,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ CallingIdentityTokenDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
)
private fun getIncidentMessageRestoreIdentityCallNotInFinallyBlock(
variableName: String
): String = "`Binder.restoreCallingIdentity($variableName)` is not an immediate child of " +
- "the `finally` block of the try statement after `$variableName` declaration. " +
- "Surround the call with `finally` block and call it unconditionally."
+ "the `finally` block of the try statement after `$variableName` declaration. " +
+ "Surround the call with `finally` block and call it unconditionally."
/** Issue: Use of caller-aware methods after Binder.clearCallingIdentity() */
@JvmField
val ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY: Issue = Issue.create(
- id = "UseOfCallerAwareMethodsWithClearedIdentity",
- briefDescription = "Use of caller-aware methods after " +
- "Binder.clearCallingIdentity()",
- explanation = """
+ id = "UseOfCallerAwareMethodsWithClearedIdentity",
+ briefDescription = "Use of caller-aware methods after " +
+ "Binder.clearCallingIdentity()",
+ explanation = """
You cleared the original calling identity with \
`Binder.clearCallingIdentity()`, but used one of the methods below before \
restoring the identity. These methods will use your own identity instead of \
@@ -556,22 +590,59 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
UserHandle.getCallingUserId()
```
""",
- category = Category.SECURITY,
- priority = 6,
- severity = Severity.WARNING,
- implementation = Implementation(
- CallingIdentityTokenDetector::class.java,
- Scope.JAVA_FILE_SCOPE
- )
+ category = Category.SECURITY,
+ priority = 6,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ CallingIdentityTokenDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
)
private fun getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity(
variableName: String,
methodName: String
): String = "You cleared the original identity with `Binder.clearCallingIdentity()` " +
- "and returned into `$variableName`, so `$methodName` will be using your own " +
- "identity instead of the caller's. Either explicitly query your own identity or " +
- "move it after restoring the identity with " +
- "`Binder.restoreCallingIdentity($variableName)`."
+ "and returned into `$variableName`, so `$methodName` will be using your own " +
+ "identity instead of the caller's. Either explicitly query your own identity or " +
+ "move it after restoring the identity with " +
+ "`Binder.restoreCallingIdentity($variableName)`."
+
+ /** Issue: Result of Binder.clearCallingIdentity() is not stored in a variable */
+ @JvmField
+ val ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE: Issue = Issue.create(
+ id = "ResultOfClearIdentityCallNotStoredInVariable",
+ briefDescription = "Result of Binder.clearCallingIdentity() is not stored in a " +
+ "variable",
+ explanation = """
+ You cleared the original calling identity with \
+ `Binder.clearCallingIdentity()`, but did not store the result of the method \
+ call in a variable. You need to store the result in a variable and restore it later.
+
+ Use the following pattern for running operations with your own identity:
+
+ ```
+ final long token = Binder.clearCallingIdentity();
+ try {
+ // Code using your own identity
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ ```
+ """,
+ category = Category.SECURITY,
+ priority = 6,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ CallingIdentityTokenDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+
+ private fun getIncidentMessageResultOfClearIdentityCallNotStoredInVariable(
+ methodName: String
+ ): String = "You cleared the original identity with `$methodName` but did not store the " +
+ "result in a variable. You need to store the result in a variable and restore it " +
+ "later."
}
}
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt
index fe567da7c017..fe567da7c017 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt
new file mode 100644
index 000000000000..48540b1da565
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint
+
+import com.android.tools.lint.client.api.UastParser
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Context
+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.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.interprocedural.CallGraph
+import com.android.tools.lint.detector.api.interprocedural.CallGraphResult
+import com.android.tools.lint.detector.api.interprocedural.searchForPaths
+import com.intellij.psi.PsiAnonymousClass
+import com.intellij.psi.PsiMethod
+import java.util.LinkedList
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UParameter
+import org.jetbrains.uast.USimpleNameReferenceExpression
+import org.jetbrains.uast.visitor.AbstractUastVisitor
+
+/**
+ * A lint checker to detect potential package visibility issues for system's APIs. APIs working
+ * in the system_server and taking the package name as a parameter may have chance to reveal
+ * package existence status on the device, and break the
+ * <a href="https://developer.android.com/about/versions/11/privacy/package-visibility">
+ * Package Visibility</a> that we introduced in Android 11.
+ * <p>
+ * Take an example of the API `boolean setFoo(String packageName)`, a malicious app may have chance
+ * to detect package existence state on the device from the result of the API, if there is no
+ * package visibility filtering rule or uid identify checks applying to the parameter of the
+ * package name.
+ */
+class PackageVisibilityDetector : Detector(), SourceCodeScanner {
+
+ // Enables call graph analysis
+ override fun isCallGraphRequired(): Boolean = true
+
+ override fun analyzeCallGraph(
+ context: Context,
+ callGraph: CallGraphResult
+ ) {
+ val systemServerApiNodes = callGraph.callGraph.nodes.filter(::isSystemServerApi)
+ val sinkMethodNodes = callGraph.callGraph.nodes.filter {
+ // TODO(b/228285232): Remove enforce permission sink methods
+ isNodeInList(it, ENFORCE_PERMISSION_METHODS) || isNodeInList(it, APPOPS_METHODS)
+ }
+ val parser = context.client.getUastParser(context.project)
+ analyzeApisContainPackageNameParameters(
+ context, parser, systemServerApiNodes, sinkMethodNodes)
+ }
+
+ /**
+ * Looking for API contains package name parameters, report the lint issue if the API does not
+ * invoke any sink methods.
+ */
+ private fun analyzeApisContainPackageNameParameters(
+ context: Context,
+ parser: UastParser,
+ systemServerApiNodes: List<CallGraph.Node>,
+ sinkMethodNodes: List<CallGraph.Node>
+ ) {
+ for (apiNode in systemServerApiNodes) {
+ val apiMethod = apiNode.getUMethod() ?: continue
+ val pkgNameParamIndexes = apiMethod.uastParameters.mapIndexedNotNull { index, param ->
+ if (Parameter(param) in PACKAGE_NAME_PATTERNS && apiNode.isArgumentInUse(index)) {
+ index
+ } else {
+ null
+ }
+ }.takeIf(List<Int>::isNotEmpty) ?: continue
+
+ for (pkgNameParamIndex in pkgNameParamIndexes) {
+ // Trace the call path of the method's argument, pass the lint checks if a sink
+ // method is found
+ if (traceArgumentCallPath(
+ apiNode, pkgNameParamIndex, PACKAGE_NAME_SINK_METHOD_LIST)) {
+ continue
+ }
+ // Pass the check if one of the sink methods is invoked
+ if (hasValidPath(
+ searchForPaths(
+ sources = listOf(apiNode),
+ isSink = { it in sinkMethodNodes },
+ getNeighbors = { node -> node.edges.map { it.node!! } }
+ )
+ )
+ ) continue
+
+ // Report issue
+ val reportElement = apiMethod.uastParameters[pkgNameParamIndex] as UElement
+ val location = parser.createLocation(reportElement)
+ context.report(
+ ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
+ location,
+ getMsgPackageNameNoPackageVisibilityFilters(apiMethod, pkgNameParamIndex)
+ )
+ }
+ }
+ }
+
+ /**
+ * Returns {@code true} if the method associated with the given node is a system server's
+ * public API that extends from Stub class.
+ */
+ private fun isSystemServerApi(
+ node: CallGraph.Node
+ ): Boolean {
+ val method = node.getUMethod() ?: return false
+ if (!method.hasModifierProperty("public") ||
+ method.uastBody == null ||
+ method.containingClass is PsiAnonymousClass) {
+ return false
+ }
+ val className = method.containingClass?.qualifiedName ?: return false
+ if (!className.startsWith(SYSTEM_PACKAGE_PREFIX)) {
+ return false
+ }
+ return (method.containingClass ?: return false).supers
+ .filter { it.name == CLASS_STUB }
+ .filter { it.qualifiedName !in BYPASS_STUBS }
+ .any { it.findMethodBySignature(method, /* checkBases */ true) != null }
+ }
+
+ /**
+ * Returns {@code true} if the list contains the node of the call graph.
+ */
+ private fun isNodeInList(
+ node: CallGraph.Node,
+ filters: List<Method>
+ ): Boolean {
+ val method = node.getUMethod() ?: return false
+ return Method(method) in filters
+ }
+
+ /**
+ * Trace the call paths of the argument of the method in the start entry. Return {@code true}
+ * if one of methods in the sink call list is invoked.
+ * Take an example of the call path:
+ * foo(packageName) -> a(packageName) -> b(packageName) -> filterAppAccess()
+ * It returns {@code true} if the filterAppAccess() is in the sink call list.
+ */
+ private fun traceArgumentCallPath(
+ apiNode: CallGraph.Node,
+ pkgNameParamIndex: Int,
+ sinkList: List<Method>
+ ): Boolean {
+ val startEntry = TraceEntry(apiNode, pkgNameParamIndex)
+ val traceQueue = LinkedList<TraceEntry>().apply { add(startEntry) }
+ val allVisits = mutableSetOf<TraceEntry>().apply { add(startEntry) }
+ while (!traceQueue.isEmpty()) {
+ val entry = traceQueue.poll()
+ val entryNode = entry.node
+ val entryMethod = entryNode.getUMethod() ?: continue
+ val entryArgumentName = entryMethod.uastParameters[entry.argumentIndex].name
+ for (outEdge in entryNode.edges) {
+ val outNode = outEdge.node ?: continue
+ val outMethod = outNode.getUMethod() ?: continue
+ val outArgumentIndex =
+ outEdge.call?.findArgumentIndex(
+ entryArgumentName, outMethod.uastParameters.size)
+ val sinkMethod = findInSinkList(outMethod, sinkList)
+ if (sinkMethod == null) {
+ if (outArgumentIndex == null) {
+ // Path is not relevant to the sink method and argument
+ continue
+ }
+ // Path is relevant to the argument, add a new trace entry if never visit before
+ val newEntry = TraceEntry(outNode, outArgumentIndex)
+ if (newEntry !in allVisits) {
+ traceQueue.add(newEntry)
+ allVisits.add(newEntry)
+ }
+ continue
+ }
+ if (sinkMethod.matchArgument && outArgumentIndex == null) {
+ // The sink call is required to match the argument, but not found
+ continue
+ }
+ if (sinkMethod.checkCaller &&
+ entryMethod.isInClearCallingIdentityScope(outEdge.call!!)) {
+ // The sink call is in the scope of Binder.clearCallingIdentify
+ continue
+ }
+ // A sink method is matched
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Returns the UMethod associated with the given node of call graph.
+ */
+ private fun CallGraph.Node.getUMethod(): UMethod? = this.target.element as? UMethod
+
+ /**
+ * Returns the system module name (e.g. com.android.server.pm) of the method of the
+ * call graph node.
+ */
+ private fun CallGraph.Node.getModuleName(): String? {
+ val method = getUMethod() ?: return null
+ val className = method.containingClass?.qualifiedName ?: return null
+ if (!className.startsWith(SYSTEM_PACKAGE_PREFIX)) {
+ return null
+ }
+ val dotPos = className.indexOf(".", SYSTEM_PACKAGE_PREFIX.length)
+ if (dotPos == -1) {
+ return SYSTEM_PACKAGE_PREFIX
+ }
+ return className.substring(0, dotPos)
+ }
+
+ /**
+ * Return {@code true} if the argument in the method's body is in-use.
+ */
+ private fun CallGraph.Node.isArgumentInUse(argIndex: Int): Boolean {
+ val method = getUMethod() ?: return false
+ val argumentName = method.uastParameters[argIndex].name
+ var foundArg = false
+ val methodVisitor = object : AbstractUastVisitor() {
+ override fun visitSimpleNameReferenceExpression(
+ node: USimpleNameReferenceExpression
+ ): Boolean {
+ if (node.identifier == argumentName) {
+ foundArg = true
+ }
+ return true
+ }
+ }
+ method.uastBody?.accept(methodVisitor)
+ return foundArg
+ }
+
+ /**
+ * Given an argument name, returns the index of argument in the call expression.
+ */
+ private fun UCallExpression.findArgumentIndex(
+ argumentName: String,
+ parameterSize: Int
+ ): Int? {
+ if (valueArgumentCount == 0 || parameterSize == 0) {
+ return null
+ }
+ var match = false
+ val argVisitor = object : AbstractUastVisitor() {
+ override fun visitSimpleNameReferenceExpression(
+ node: USimpleNameReferenceExpression
+ ): Boolean {
+ if (node.identifier == argumentName) {
+ match = true
+ }
+ return true
+ }
+ override fun visitCallExpression(node: UCallExpression): Boolean {
+ return true
+ }
+ }
+ valueArguments.take(parameterSize).forEachIndexed { index, argument ->
+ argument.accept(argVisitor)
+ if (match) {
+ return index
+ }
+ }
+ return null
+ }
+
+ /**
+ * Given a UMethod, returns a method from the sink method list.
+ */
+ private fun findInSinkList(
+ uMethod: UMethod,
+ sinkCallList: List<Method>
+ ): Method? {
+ return sinkCallList.find {
+ it == Method(uMethod) ||
+ it == Method(uMethod.containingClass?.qualifiedName ?: "", "*")
+ }
+ }
+
+ /**
+ * Returns {@code true} if the call expression is in the scope of the
+ * Binder.clearCallingIdentify.
+ */
+ private fun UMethod.isInClearCallingIdentityScope(call: UCallExpression): Boolean {
+ var isInScope = false
+ val methodVisitor = object : AbstractUastVisitor() {
+ private var clearCallingIdentity = 0
+ override fun visitCallExpression(node: UCallExpression): Boolean {
+ if (call == node && clearCallingIdentity != 0) {
+ isInScope = true
+ return true
+ }
+ val visitMethod = Method(node.resolve() ?: return false)
+ if (visitMethod == METHOD_CLEAR_CALLING_IDENTITY) {
+ clearCallingIdentity++
+ } else if (visitMethod == METHOD_RESTORE_CALLING_IDENTITY) {
+ clearCallingIdentity--
+ }
+ return false
+ }
+ }
+ accept(methodVisitor)
+ return isInScope
+ }
+
+ /**
+ * Checks the module name of the start node and the last node that invokes the sink method
+ * (e.g. checkPermission) in a path, returns {@code true} if one of the paths has the same
+ * module name for both nodes.
+ */
+ private fun hasValidPath(paths: Collection<List<CallGraph.Node>>): Boolean {
+ for (pathNodes in paths) {
+ if (pathNodes.size < VALID_CALL_PATH_NODES_SIZE) {
+ continue
+ }
+ val startModule = pathNodes[0].getModuleName() ?: continue
+ val lastCallModule = pathNodes[pathNodes.size - 2].getModuleName() ?: continue
+ if (startModule == lastCallModule) {
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * A data class to represent the method.
+ */
+ private data class Method(
+ val clazz: String,
+ val name: String
+ ) {
+ // Used by traceArgumentCallPath to indicate that the method is required to match the
+ // argument name
+ var matchArgument = true
+
+ // Used by traceArgumentCallPath to indicate that the method is required to check whether
+ // the Binder.clearCallingIdentity is invoked.
+ var checkCaller = false
+
+ constructor(
+ clazz: String,
+ name: String,
+ matchArgument: Boolean = true,
+ checkCaller: Boolean = false
+ ) : this(clazz, name) {
+ this.matchArgument = matchArgument
+ this.checkCaller = checkCaller
+ }
+
+ constructor(
+ method: PsiMethod
+ ) : this(method.containingClass?.qualifiedName ?: "", method.name)
+
+ constructor(
+ method: com.google.android.lint.model.Method
+ ) : this(method.clazz, method.name)
+ }
+
+ /**
+ * A data class to represent the parameter of the method. The parameter name is converted to
+ * lower case letters for comparison.
+ */
+ private data class Parameter private constructor(
+ val typeName: String,
+ val parameterName: String
+ ) {
+ constructor(uParameter: UParameter) : this(
+ uParameter.type.canonicalText,
+ uParameter.name.lowercase()
+ )
+
+ companion object {
+ fun create(typeName: String, parameterName: String) =
+ Parameter(typeName, parameterName.lowercase())
+ }
+ }
+
+ /**
+ * A data class wraps a method node of the call graph and an index that indicates an
+ * argument of the method to record call trace information.
+ */
+ private data class TraceEntry(
+ val node: CallGraph.Node,
+ val argumentIndex: Int
+ )
+
+ companion object {
+ private const val SYSTEM_PACKAGE_PREFIX = "com.android.server."
+ // A valid call path list needs to contain a start node and a sink node
+ private const val VALID_CALL_PATH_NODES_SIZE = 2
+
+ private const val CLASS_STRING = "java.lang.String"
+ private const val CLASS_PACKAGE_MANAGER = "android.content.pm.PackageManager"
+ private const val CLASS_IPACKAGE_MANAGER = "android.content.pm.IPackageManager"
+ private const val CLASS_APPOPS_MANAGER = "android.app.AppOpsManager"
+ private const val CLASS_BINDER = "android.os.Binder"
+ private const val CLASS_PACKAGE_MANAGER_INTERNAL =
+ "android.content.pm.PackageManagerInternal"
+
+ // Patterns of package name parameter
+ private val PACKAGE_NAME_PATTERNS = setOf(
+ Parameter.create(CLASS_STRING, "packageName"),
+ Parameter.create(CLASS_STRING, "callingPackage"),
+ Parameter.create(CLASS_STRING, "callingPackageName"),
+ Parameter.create(CLASS_STRING, "pkgName"),
+ Parameter.create(CLASS_STRING, "callingPkg"),
+ Parameter.create(CLASS_STRING, "pkg")
+ )
+
+ // Package manager APIs
+ private val PACKAGE_NAME_SINK_METHOD_LIST = listOf(
+ Method(CLASS_PACKAGE_MANAGER_INTERNAL, "filterAppAccess", matchArgument = false),
+ Method(CLASS_PACKAGE_MANAGER_INTERNAL, "canQueryPackage"),
+ Method(CLASS_PACKAGE_MANAGER_INTERNAL, "isSameApp"),
+ Method(CLASS_PACKAGE_MANAGER, "*", checkCaller = true),
+ Method(CLASS_IPACKAGE_MANAGER, "*", checkCaller = true),
+ Method(CLASS_PACKAGE_MANAGER, "getPackagesForUid", matchArgument = false),
+ Method(CLASS_IPACKAGE_MANAGER, "getPackagesForUid", matchArgument = false)
+ )
+
+ // AppOps APIs which include uid and package visibility filters checks
+ private val APPOPS_METHODS = listOf(
+ Method(CLASS_APPOPS_MANAGER, "noteOp"),
+ Method(CLASS_APPOPS_MANAGER, "noteOpNoThrow"),
+ Method(CLASS_APPOPS_MANAGER, "noteOperation"),
+ Method(CLASS_APPOPS_MANAGER, "noteProxyOp"),
+ Method(CLASS_APPOPS_MANAGER, "noteProxyOpNoThrow"),
+ Method(CLASS_APPOPS_MANAGER, "startOp"),
+ Method(CLASS_APPOPS_MANAGER, "startOpNoThrow"),
+ Method(CLASS_APPOPS_MANAGER, "FinishOp"),
+ Method(CLASS_APPOPS_MANAGER, "finishProxyOp"),
+ Method(CLASS_APPOPS_MANAGER, "checkPackage")
+ )
+
+ // Enforce permission APIs
+ private val ENFORCE_PERMISSION_METHODS =
+ com.google.android.lint.ENFORCE_PERMISSION_METHODS
+ .map(PackageVisibilityDetector::Method)
+
+ private val BYPASS_STUBS = listOf(
+ "android.content.pm.IPackageDataObserver.Stub",
+ "android.content.pm.IPackageDeleteObserver.Stub",
+ "android.content.pm.IPackageDeleteObserver2.Stub",
+ "android.content.pm.IPackageInstallObserver2.Stub",
+ "com.android.internal.app.IAppOpsCallback.Stub",
+
+ // TODO(b/228285637): Do not bypass PackageManagerService API
+ "android.content.pm.IPackageManager.Stub",
+ "android.content.pm.IPackageManagerNative.Stub"
+ )
+
+ private val METHOD_CLEAR_CALLING_IDENTITY =
+ Method(CLASS_BINDER, "clearCallingIdentity")
+ private val METHOD_RESTORE_CALLING_IDENTITY =
+ Method(CLASS_BINDER, "restoreCallingIdentity")
+
+ private fun getMsgPackageNameNoPackageVisibilityFilters(
+ method: UMethod,
+ argumentIndex: Int
+ ): String = "Api: ${method.name} contains a package name parameter: " +
+ "${method.uastParameters[argumentIndex].name} does not apply " +
+ "package visibility filtering rules."
+
+ private val EXPLANATION = """
+ APIs working in the system_server and taking the package name as a parameter may have
+ chance to reveal package existence status on the device, and break the package
+ visibility that we introduced in Android 11.
+ (https://developer.android.com/about/versions/11/privacy/package-visibility)
+
+ Take an example of the API `boolean setFoo(String packageName)`, a malicious app may
+ have chance to get package existence state on the device from the result of the API,
+ if there is no package visibility filtering rule or uid identify checks applying to
+ the parameter of the package name.
+
+ To resolve it, you could apply package visibility filtering rules to the package name
+ via PackageManagerInternal.filterAppAccess API, before starting to use the package name.
+ If the parameter is a calling package name, use the PackageManager API such as
+ PackageManager.getPackagesForUid to verify the calling identify.
+ """
+
+ val ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS = Issue.create(
+ id = "ApiMightLeakAppVisibility",
+ briefDescription = "Api takes package name parameter doesn't apply " +
+ "package visibility filters",
+ explanation = EXPLANATION,
+ category = Category.SECURITY,
+ priority = 1,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ PackageVisibilityDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+ }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
new file mode 100644
index 000000000000..e12ec3d4a77c
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+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.getUMethod
+import com.intellij.psi.PsiType
+import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UIfExpression
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UQualifiedReferenceExpression
+import org.jetbrains.uast.UReturnExpression
+import org.jetbrains.uast.getContainingUMethod
+
+/**
+ * Stops incorrect usage of {@link PermissionMethod}
+ * TODO: add tests once re-enabled (b/240445172, b/247542171)
+ */
+class PermissionMethodDetector : Detector(), SourceCodeScanner {
+
+ override fun getApplicableUastTypes(): List<Class<out UElement>> =
+ listOf(UAnnotation::class.java, UMethod::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler =
+ PermissionMethodHandler(context)
+
+ private inner class PermissionMethodHandler(val context: JavaContext) : UElementHandler() {
+ override fun visitMethod(node: UMethod) {
+ if (hasPermissionMethodAnnotation(node)) return
+ if (onlyCallsPermissionMethod(node)) {
+ val location = context.getLocation(node.javaPsi.modifierList)
+ val fix = fix()
+ .annotate(ANNOTATION_PERMISSION_METHOD)
+ .range(location)
+ .autoFix()
+ .build()
+
+ context.report(
+ ISSUE_CAN_BE_PERMISSION_METHOD,
+ location,
+ "Annotate method with @PermissionMethod",
+ fix
+ )
+ }
+ }
+
+ override fun visitAnnotation(node: UAnnotation) {
+ if (node.qualifiedName != ANNOTATION_PERMISSION_METHOD) return
+ val method = node.getContainingUMethod() ?: return
+
+ if (!isPermissionMethodReturnType(method)) {
+ context.report(
+ ISSUE_PERMISSION_METHOD_USAGE,
+ context.getLocation(node),
+ """
+ Methods annotated with `@PermissionMethod` should return `void`, \
+ `boolean`, or `@PackageManager.PermissionResult int`."
+ """.trimIndent()
+ )
+ }
+
+ if (method.returnType == PsiType.INT &&
+ method.annotations.none { it.hasQualifiedName(ANNOTATION_PERMISSION_RESULT) }
+ ) {
+ context.report(
+ ISSUE_PERMISSION_METHOD_USAGE,
+ context.getLocation(node),
+ """
+ Methods annotated with `@PermissionMethod` that return `int` should \
+ also be annotated with `@PackageManager.PermissionResult.`"
+ """.trimIndent()
+ )
+ }
+ }
+ }
+
+ companion object {
+
+ private val EXPLANATION_PERMISSION_METHOD_USAGE = """
+ `@PermissionMethod` should annotate methods that ONLY perform permission lookups. \
+ Said methods should return `boolean`, `@PackageManager.PermissionResult int`, or return \
+ `void` and potentially throw `SecurityException`.
+ """.trimIndent()
+
+ @JvmField
+ val ISSUE_PERMISSION_METHOD_USAGE = Issue.create(
+ id = "PermissionMethodUsage",
+ briefDescription = "@PermissionMethod used incorrectly",
+ explanation = EXPLANATION_PERMISSION_METHOD_USAGE,
+ category = Category.CORRECTNESS,
+ priority = 5,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ PermissionMethodDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ ),
+ enabledByDefault = true
+ )
+
+ private val EXPLANATION_CAN_BE_PERMISSION_METHOD = """
+ Methods that only call other methods annotated with @PermissionMethod (and do NOTHING else) can themselves \
+ be annotated with @PermissionMethod. For example:
+ ```
+ void wrapperHelper() {
+ // Context.enforceCallingPermission is annotated with @PermissionMethod
+ context.enforceCallingPermission(SOME_PERMISSION)
+ }
+ ```
+ """.trimIndent()
+
+ @JvmField
+ val ISSUE_CAN_BE_PERMISSION_METHOD = Issue.create(
+ id = "CanBePermissionMethod",
+ briefDescription = "Method can be annotated with @PermissionMethod",
+ explanation = EXPLANATION_CAN_BE_PERMISSION_METHOD,
+ category = Category.SECURITY,
+ priority = 5,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ PermissionMethodDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ ),
+ enabledByDefault = false
+ )
+
+ private fun isPermissionMethodReturnType(method: UMethod): Boolean =
+ listOf(PsiType.VOID, PsiType.INT, PsiType.BOOLEAN).contains(method.returnType)
+
+ /**
+ * Identifies methods that...
+ * DO call other methods annotated with @PermissionMethod
+ * DO NOT do anything else
+ */
+ private fun onlyCallsPermissionMethod(method: UMethod): Boolean {
+ val body = method.uastBody as? UBlockExpression ?: return false
+ if (body.expressions.isEmpty()) return false
+ for (expression in body.expressions) {
+ when (expression) {
+ is UQualifiedReferenceExpression -> {
+ if (!isPermissionMethodCall(expression.selector)) return false
+ }
+ is UReturnExpression -> {
+ if (!isPermissionMethodCall(expression.returnExpression)) return false
+ }
+ is UCallExpression -> {
+ if (!isPermissionMethodCall(expression)) return false
+ }
+ is UIfExpression -> {
+ if (expression.thenExpression !is UReturnExpression) return false
+ if (!isPermissionMethodCall(expression.condition)) return false
+ }
+ else -> return false
+ }
+ }
+ return true
+ }
+
+ private fun isPermissionMethodCall(expression: UExpression?): Boolean {
+ return when (expression) {
+ is UQualifiedReferenceExpression ->
+ return isPermissionMethodCall(expression.selector)
+ is UCallExpression -> {
+ val calledMethod = expression.resolve()?.getUMethod() ?: return false
+ return hasPermissionMethodAnnotation(calledMethod)
+ }
+ else -> false
+ }
+ }
+
+ private fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations
+ .any { it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD) }
+ }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt
new file mode 100644
index 000000000000..06c098df385d
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.parcel
+
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.LintFix
+import com.android.tools.lint.detector.api.Location
+import com.intellij.psi.PsiArrayType
+import com.intellij.psi.PsiCallExpression
+import com.intellij.psi.PsiClassType
+import com.intellij.psi.PsiIntersectionType
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiType
+import com.intellij.psi.PsiTypeParameter
+import com.intellij.psi.PsiWildcardType
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UVariable
+
+/**
+ * Subclass this class and override {@link #getBoundingClass} to report an unsafe Parcel API issue
+ * with a fix that migrates towards the new safer API by appending an argument in the form of
+ * {@code com.package.ItemType.class} coming from the result of the overridden method.
+ */
+abstract class CallMigrator(
+ val method: Method,
+ private val rejects: Set<String> = emptySet(),
+) {
+ open fun report(context: JavaContext, call: UCallExpression, method: PsiMethod) {
+ val location = context.getLocation(call)
+ val itemType = filter(getBoundingClass(context, call, method))
+ val fix = (itemType as? PsiClassType)?.let { type ->
+ getParcelFix(location, this.method.name, getArgumentSuffix(type))
+ }
+ val message = "Unsafe `${this.method.className}.${this.method.name}()` API usage"
+ context.report(SaferParcelChecker.ISSUE_UNSAFE_API_USAGE, call, location, message, fix)
+ }
+
+ protected open fun getArgumentSuffix(type: PsiClassType) =
+ ", ${type.rawType().canonicalText}.class"
+
+ protected open fun getBoundingClass(
+ context: JavaContext,
+ call: UCallExpression,
+ method: PsiMethod,
+ ): PsiType? = null
+
+ protected fun getItemType(type: PsiType, container: String): PsiClassType? {
+ val supers = getParentTypes(type).mapNotNull { it as? PsiClassType }
+ val containerType = supers.firstOrNull { it.rawType().canonicalText == container }
+ ?: return null
+ val itemType = containerType.parameters.getOrNull(0) ?: return null
+ // TODO: Expand to other types, see PsiTypeVisitor
+ return when (itemType) {
+ is PsiClassType -> itemType
+ is PsiWildcardType -> itemType.bound as PsiClassType
+ else -> null
+ }
+ }
+
+ /**
+ * Tries to obtain the type expected by the "receiving" end given a certain {@link UElement}.
+ *
+ * This could be an assignment, an argument passed to a method call, to a constructor call, a
+ * type cast, etc. If no receiving end is found, the type of the UExpression itself is returned.
+ */
+ protected fun getReceivingType(expression: UElement): PsiType? {
+ val parent = expression.uastParent
+ var type = when (parent) {
+ is UCallExpression -> {
+ val i = parent.valueArguments.indexOf(expression)
+ val psiCall = parent.sourcePsi as? PsiCallExpression ?: return null
+ val typeSubstitutor = psiCall.resolveMethodGenerics().substitutor
+ val method = psiCall.resolveMethod()!!
+ method.getSignature(typeSubstitutor).parameterTypes[i]
+ }
+ is UVariable -> parent.type
+ is UExpression -> parent.getExpressionType()
+ else -> null
+ }
+ if (type == null && expression is UExpression) {
+ type = expression.getExpressionType()
+ }
+ return type
+ }
+
+ protected fun filter(type: PsiType?): PsiType? {
+ // It's important that PsiIntersectionType case is above the one that check the type in
+ // rejects, because for intersect types, the canonicalText is one of the terms.
+ if (type is PsiIntersectionType) {
+ return type.conjuncts.mapNotNull(this::filter).firstOrNull()
+ }
+ if (type == null || type.canonicalText in rejects) {
+ return null
+ }
+ if (type is PsiClassType && type.resolve() is PsiTypeParameter) {
+ return null
+ }
+ return type
+ }
+
+ private fun getParentTypes(type: PsiType): Set<PsiType> =
+ type.superTypes.flatMap(::getParentTypes).toSet() + type
+
+ protected fun getParcelFix(location: Location, method: String, arguments: String) =
+ LintFix
+ .create()
+ .name("Migrate to safer Parcel.$method() API")
+ .replace()
+ .range(location)
+ .pattern("$method\\s*\\(((?:.|\\n)*)\\)")
+ .with("\\k<1>$arguments")
+ .autoFix()
+ .build()
+}
+
+/**
+ * This class derives the type to be appended by inferring the generic type of the {@code container}
+ * type (eg. "java.util.List") of the {@code argument}-th argument.
+ */
+class ContainerArgumentMigrator(
+ method: Method,
+ private val argument: Int,
+ private val container: String,
+ rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+ override fun getBoundingClass(
+ context: JavaContext, call: UCallExpression, method: PsiMethod
+ ): PsiType? {
+ val firstParamType = call.valueArguments[argument].getExpressionType() ?: return null
+ return getItemType(firstParamType, container)!!
+ }
+
+ /**
+ * We need to insert a casting construct in the class parameter. For example:
+ * (Class<Foo<Bar>>) (Class<?>) Foo.class.
+ * This is needed for when the arguments of the conflict (eg. when there is List<Foo<Bar>> and
+ * class type is Class<Foo?).
+ */
+ override fun getArgumentSuffix(type: PsiClassType): String {
+ if (type.parameters.isNotEmpty()) {
+ val rawType = type.rawType()
+ return ", (Class<${type.canonicalText}>) (Class<?>) ${rawType.canonicalText}.class"
+ }
+ return super.getArgumentSuffix(type)
+ }
+}
+
+/**
+ * This class derives the type to be appended by inferring the generic type of the {@code container}
+ * type (eg. "java.util.List") of the return type of the method.
+ */
+class ContainerReturnMigrator(
+ method: Method,
+ private val container: String,
+ rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+ override fun getBoundingClass(
+ context: JavaContext, call: UCallExpression, method: PsiMethod
+ ): PsiType? {
+ val type = getReceivingType(call.uastParent!!) ?: return null
+ return getItemType(type, container)
+ }
+}
+
+/**
+ * This class derives the type to be appended by inferring the expected type for the method result.
+ */
+class ReturnMigrator(
+ method: Method,
+ rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+ override fun getBoundingClass(
+ context: JavaContext, call: UCallExpression, method: PsiMethod
+ ): PsiType? {
+ return getReceivingType(call.uastParent!!)
+ }
+}
+
+/**
+ * This class appends the class loader and the class object by deriving the type from the method
+ * result.
+ */
+class ReturnMigratorWithClassLoader(
+ method: Method,
+ rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+ override fun getBoundingClass(
+ context: JavaContext, call: UCallExpression, method: PsiMethod
+ ): PsiType? {
+ return getReceivingType(call.uastParent!!)
+ }
+
+ override fun getArgumentSuffix(type: PsiClassType): String =
+ "${type.rawType().canonicalText}.class.getClassLoader(), " +
+ "${type.rawType().canonicalText}.class"
+
+}
+
+/**
+ * This class derives the type to be appended by inferring the expected array type
+ * for the method result.
+ */
+class ArrayReturnMigrator(
+ method: Method,
+ rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+ override fun getBoundingClass(
+ context: JavaContext, call: UCallExpression, method: PsiMethod
+ ): PsiType? {
+ val type = getReceivingType(call.uastParent!!)
+ return (type as? PsiArrayType)?.componentType
+ }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt
new file mode 100644
index 000000000000..0826e8e74431
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.parcel
+
+data class Method(
+ val params: List<String>,
+ val clazz: String,
+ val name: String,
+ val parameters: List<String>
+) {
+ constructor(
+ clazz: String,
+ name: String,
+ parameters: List<String>
+ ) : this(
+ listOf(), clazz, name, parameters
+ )
+
+ val signature: String
+ get() {
+ val prefix = if (params.isEmpty()) "" else "${params.joinToString(", ", "<", ">")} "
+ return "$prefix$clazz.$name(${parameters.joinToString()})"
+ }
+
+ val className: String by lazy {
+ clazz.split(".").last()
+ }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt
new file mode 100644
index 000000000000..f92826316be4
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.parcel
+
+import com.android.tools.lint.detector.api.*
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiSubstitutor
+import com.intellij.psi.PsiType
+import com.intellij.psi.PsiTypeParameter
+import org.jetbrains.uast.UCallExpression
+import java.util.*
+
+@Suppress("UnstableApiUsage")
+class SaferParcelChecker : Detector(), SourceCodeScanner {
+ override fun getApplicableMethodNames(): List<String> =
+ MIGRATORS
+ .map(CallMigrator::method)
+ .map(Method::name)
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (!isAtLeastT(context)) return
+ val signature = getSignature(method)
+ val migrator = MIGRATORS.firstOrNull { it.method.signature == signature } ?: return
+ migrator.report(context, node, method)
+ }
+
+ private fun getSignature(method: PsiMethod): String {
+ val name = UastLintUtils.getQualifiedName(method)
+ val signature = method.getSignature(PsiSubstitutor.EMPTY)
+ val parameters =
+ signature.parameterTypes.joinToString(transform = PsiType::getCanonicalText)
+ val types = signature.typeParameters.map(PsiTypeParameter::getName)
+ val prefix = if (types.isEmpty()) "" else types.joinToString(", ", "<", ">") + " "
+ return "$prefix$name($parameters)"
+ }
+
+ private fun isAtLeastT(context: Context): Boolean {
+ val project = if (context.isGlobalAnalysis()) context.mainProject else context.project
+ return project.isAndroidProject && project.minSdkVersion.featureLevel >= 33
+ }
+
+ companion object {
+ @JvmField
+ val ISSUE_UNSAFE_API_USAGE: Issue = Issue.create(
+ id = "UnsafeParcelApi",
+ briefDescription = "Use of unsafe deserialization API",
+ explanation = """
+ You are using a deprecated deserialization API that doesn't accept the expected class as\
+ a parameter. This means that unexpected classes could be instantiated and\
+ unexpected code executed.
+
+ Please migrate to the safer alternative that takes an extra Class<T> parameter.
+ """,
+ category = Category.SECURITY,
+ priority = 8,
+ severity = Severity.WARNING,
+
+ implementation = Implementation(
+ SaferParcelChecker::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+
+ // Parcel
+ private val PARCEL_METHOD_READ_SERIALIZABLE = Method("android.os.Parcel", "readSerializable", listOf())
+ private val PARCEL_METHOD_READ_ARRAY_LIST = Method("android.os.Parcel", "readArrayList", listOf("java.lang.ClassLoader"))
+ private val PARCEL_METHOD_READ_LIST = Method("android.os.Parcel", "readList", listOf("java.util.List", "java.lang.ClassLoader"))
+ private val PARCEL_METHOD_READ_PARCELABLE = Method(listOf("T"), "android.os.Parcel", "readParcelable", listOf("java.lang.ClassLoader"))
+ private val PARCEL_METHOD_READ_PARCELABLE_LIST = Method(listOf("T"), "android.os.Parcel", "readParcelableList", listOf("java.util.List<T>", "java.lang.ClassLoader"))
+ private val PARCEL_METHOD_READ_SPARSE_ARRAY = Method(listOf("T"), "android.os.Parcel", "readSparseArray", listOf("java.lang.ClassLoader"))
+ private val PARCEL_METHOD_READ_ARRAY = Method("android.os.Parcel", "readArray", listOf("java.lang.ClassLoader"))
+ private val PARCEL_METHOD_READ_PARCELABLE_ARRAY = Method("android.os.Parcel", "readParcelableArray", listOf("java.lang.ClassLoader"))
+
+ // Bundle
+ private val BUNDLE_METHOD_GET_SERIALIZABLE = Method("android.os.Bundle", "getSerializable", listOf("java.lang.String"))
+ private val BUNDLE_METHOD_GET_PARCELABLE = Method(listOf("T"), "android.os.Bundle", "getParcelable", listOf("java.lang.String"))
+ private val BUNDLE_METHOD_GET_PARCELABLE_ARRAY_LIST = Method(listOf("T"), "android.os.Bundle", "getParcelableArrayList", listOf("java.lang.String"))
+ private val BUNDLE_METHOD_GET_PARCELABLE_ARRAY = Method("android.os.Bundle", "getParcelableArray", listOf("java.lang.String"))
+ private val BUNDLE_METHOD_GET_SPARSE_PARCELABLE_ARRAY = Method(listOf("T"), "android.os.Bundle", "getSparseParcelableArray", listOf("java.lang.String"))
+
+ // Intent
+ private val INTENT_METHOD_GET_SERIALIZABLE_EXTRA = Method("android.content.Intent", "getSerializableExtra", listOf("java.lang.String"))
+ private val INTENT_METHOD_GET_PARCELABLE_EXTRA = Method(listOf("T"), "android.content.Intent", "getParcelableExtra", listOf("java.lang.String"))
+ private val INTENT_METHOD_GET_PARCELABLE_ARRAY_EXTRA = Method("android.content.Intent", "getParcelableArrayExtra", listOf("java.lang.String"))
+ private val INTENT_METHOD_GET_PARCELABLE_ARRAY_LIST_EXTRA = Method(listOf("T"), "android.content.Intent", "getParcelableArrayListExtra", listOf("java.lang.String"))
+
+ // TODO: Write migrators for methods below
+ private val PARCEL_METHOD_READ_PARCELABLE_CREATOR = Method("android.os.Parcel", "readParcelableCreator", listOf("java.lang.ClassLoader"))
+
+ private val MIGRATORS = listOf(
+ ReturnMigrator(PARCEL_METHOD_READ_PARCELABLE, setOf("android.os.Parcelable")),
+ ContainerArgumentMigrator(PARCEL_METHOD_READ_LIST, 0, "java.util.List"),
+ ContainerReturnMigrator(PARCEL_METHOD_READ_ARRAY_LIST, "java.util.Collection"),
+ ContainerReturnMigrator(PARCEL_METHOD_READ_SPARSE_ARRAY, "android.util.SparseArray"),
+ ContainerArgumentMigrator(PARCEL_METHOD_READ_PARCELABLE_LIST, 0, "java.util.List"),
+ ReturnMigratorWithClassLoader(PARCEL_METHOD_READ_SERIALIZABLE),
+ ArrayReturnMigrator(PARCEL_METHOD_READ_ARRAY, setOf("java.lang.Object")),
+ ArrayReturnMigrator(PARCEL_METHOD_READ_PARCELABLE_ARRAY, setOf("android.os.Parcelable")),
+
+ ReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE, setOf("android.os.Parcelable")),
+ ContainerReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE_ARRAY_LIST, "java.util.Collection", setOf("android.os.Parcelable")),
+ ArrayReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE_ARRAY, setOf("android.os.Parcelable")),
+ ContainerReturnMigrator(BUNDLE_METHOD_GET_SPARSE_PARCELABLE_ARRAY, "android.util.SparseArray", setOf("android.os.Parcelable")),
+ ReturnMigrator(BUNDLE_METHOD_GET_SERIALIZABLE, setOf("java.io.Serializable")),
+
+ ReturnMigrator(INTENT_METHOD_GET_PARCELABLE_EXTRA, setOf("android.os.Parcelable")),
+ ContainerReturnMigrator(INTENT_METHOD_GET_PARCELABLE_ARRAY_LIST_EXTRA, "java.util.Collection", setOf("android.os.Parcelable")),
+ ArrayReturnMigrator(INTENT_METHOD_GET_PARCELABLE_ARRAY_EXTRA, setOf("android.os.Parcelable")),
+ ReturnMigrator(INTENT_METHOD_GET_SERIALIZABLE_EXTRA, setOf("java.io.Serializable")),
+ )
+ }
+}
diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt
index e1a5c613dee1..d90f3e31baf9 100644
--- a/tools/lint/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt
+++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt
@@ -27,12 +27,13 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
override fun getDetector(): Detector = CallingIdentityTokenDetector()
override fun getIssues(): List<Issue> = listOf(
- CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN,
- CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN,
- CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
- CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
- CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
- CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY
+ CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN,
+ CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN,
+ CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
+ CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
+ CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
+ CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
+ CallingIdentityTokenDetector.ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE
)
override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
@@ -41,8 +42,8 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
fun testDoesNotDetectIssuesInCorrectScenario() {
lint().files(
- java(
- """
+ java(
+ """
package test.pkg;
import android.os.Binder;
public class TestClass1 extends Binder {
@@ -62,22 +63,29 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
} finally {
restoreCallingIdentity(token3);
}
+ final Long token4 = true ? Binder.clearCallingIdentity() : null;
+ try {
+ } finally {
+ if (token4 != null) {
+ restoreCallingIdentity(token4);
+ }
+ }
}
}
"""
- ).indented(),
- *stubs
+ ).indented(),
+ *stubs
)
- .run()
- .expectClean()
+ .run()
+ .expectClean()
}
/** Unused token issue tests */
fun testDetectsUnusedTokens() {
lint().files(
- java(
- """
+ java(
+ """
package test.pkg;
import android.os.Binder;
public class TestClass1 extends Binder {
@@ -101,12 +109,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
}
}
"""
- ).indented(),
- *stubs
+ ).indented(),
+ *stubs
)
- .run()
- .expect(
- """
+ .run()
+ .expect(
+ """
src/test/pkg/TestClass1.java:5: Warning: token1 has not been used to \
restore the calling identity. Introduce a try-finally after the \
declaration and call Binder.restoreCallingIdentity(token1) in finally or \
@@ -127,13 +135,13 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 3 warnings
""".addLineContinuation()
- )
+ )
}
fun testDetectsUnusedTokensInScopes() {
lint().files(
- java(
- """
+ java(
+ """
package test.pkg;
import android.os.Binder;
public class TestClass1 {
@@ -152,12 +160,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
}
}
"""
- ).indented(),
- *stubs
+ ).indented(),
+ *stubs
)
- .run()
- .expect(
- """
+ .run()
+ .expect(
+ """
src/test/pkg/TestClass1.java:5: Warning: token has not been used to \
restore the calling identity. Introduce a try-finally after the \
declaration and call Binder.restoreCallingIdentity(token) in finally or \
@@ -166,13 +174,13 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 1 warnings
""".addLineContinuation()
- )
+ )
}
fun testDoesNotDetectUsedTokensInScopes() {
lint().files(
- java(
- """
+ java(
+ """
package test.pkg;
import android.os.Binder;
public class TestClass1 {
@@ -192,17 +200,17 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
}
}
"""
- ).indented(),
- *stubs
+ ).indented(),
+ *stubs
)
- .run()
- .expectClean()
+ .run()
+ .expectClean()
}
fun testDetectsUnusedTokensWithSimilarNamesInScopes() {
lint().files(
- java(
- """
+ java(
+ """
package test.pkg;
import android.os.Binder;
public class TestClass1 {
@@ -220,12 +228,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
}
}
"""
- ).indented(),
- *stubs
+ ).indented(),
+ *stubs
)
- .run()
- .expect(
- """
+ .run()
+ .expect(
+ """
src/test/pkg/TestClass1.java:5: Warning: token has not been used to \
restore the calling identity. Introduce a try-finally after the \
declaration and call Binder.restoreCallingIdentity(token) in finally or \
@@ -240,15 +248,15 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 2 warnings
""".addLineContinuation()
- )
+ )
}
/** Non-final token issue tests */
fun testDetectsNonFinalTokens() {
lint().files(
- java(
- """
+ java(
+ """
package test.pkg;
import android.os.Binder;
public class TestClass1 extends Binder {
@@ -271,12 +279,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
}
}
"""
- ).indented(),
- *stubs
+ ).indented(),
+ *stubs
)
- .run()
- .expect(
- """
+ .run()
+ .expect(
+ """
src/test/pkg/TestClass1.java:5: Warning: token1 is a non-final token from \
Binder.clearCallingIdentity(). Add final keyword to token1. \
[NonFinalTokenOfOriginalCallingIdentity]
@@ -294,7 +302,7 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 3 warnings
""".addLineContinuation()
- )
+ )
}
/** Nested clearCallingIdentity() calls issue tests */
@@ -302,8 +310,8 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
fun testDetectsNestedClearCallingIdentityCalls() {
// Pattern: clear - clear - clear - restore - restore - restore
lint().files(
- java(
- """
+ java(
+ """
package test.pkg;
import android.os.Binder;
public class TestClass1 extends Binder {
@@ -326,12 +334,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
}
}
"""
- ).indented(),
- *stubs
+ ).indented(),
+ *stubs
)
- .run()
- .expect(
- """
+ .run()
+ .expect(
+ """
src/test/pkg/TestClass1.java:7: Warning: The calling identity has already \
been cleared and returned into token1. Move token2 declaration after \
restoring the calling identity with Binder.restoreCallingIdentity(token1). \
@@ -348,15 +356,15 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
src/test/pkg/TestClass1.java:5: Location of the token1 declaration.
0 errors, 2 warnings
""".addLineContinuation()
- )
+ )
}
/** clearCallingIdentity() not followed by try-finally issue tests */
fun testDetectsClearIdentityCallNotFollowedByTryFinally() {
lint().files(
- java(
- """
+ java(
+ """
package test.pkg;
import android.os.Binder;
public class TestClass1 extends Binder{
@@ -397,12 +405,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
}
}
"""
- ).indented(),
- *stubs
+ ).indented(),
+ *stubs
)
- .run()
- .expect(
- """
+ .run()
+ .expect(
+ """
src/test/pkg/TestClass1.java:5: Warning: You cleared the calling identity \
and returned the result into token, but the next statement is not a \
try-finally statement. Define a try-finally block after token declaration \
@@ -445,15 +453,15 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 5 warnings
""".addLineContinuation()
- )
+ )
}
/** restoreCallingIdentity() call not in finally block issue tests */
fun testDetectsRestoreCallingIdentityCallNotInFinally() {
lint().files(
- java(
- """
+ java(
+ """
package test.pkg;
import android.os.Binder;
public class TestClass1 extends Binder {
@@ -482,12 +490,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
}
}
"""
- ).indented(),
- *stubs
+ ).indented(),
+ *stubs
)
- .run()
- .expect(
- """
+ .run()
+ .expect(
+ """
src/test/pkg/TestClass1.java:10: Warning: \
Binder.restoreCallingIdentity(token) is not an immediate child of the \
finally block of the try statement after token declaration. Surround the c\
@@ -511,13 +519,13 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 3 warnings
""".addLineContinuation()
- )
+ )
}
fun testDetectsRestoreCallingIdentityCallNotInFinallyInScopes() {
lint().files(
- java(
- """
+ java(
+ """
package test.pkg;
import android.os.Binder;
public class TestClass1 extends Binder {
@@ -560,12 +568,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
}
}
"""
- ).indented(),
- *stubs
+ ).indented(),
+ *stubs
)
- .run()
- .expect(
- """
+ .run()
+ .expect(
+ """
src/test/pkg/TestClass1.java:11: Warning: \
Binder.restoreCallingIdentity(token1) is not an immediate child of the \
finally block of the try statement after token1 declaration. Surround the \
@@ -596,15 +604,15 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 4 warnings
""".addLineContinuation()
- )
+ )
}
/** Use of caller-aware methods after clearCallingIdentity() issue tests */
fun testDetectsUseOfCallerAwareMethodsWithClearedIdentityIssuesInScopes() {
lint().files(
- java(
- """
+ java(
+ """
package test.pkg;
import android.os.Binder;
import android.os.UserHandle;
@@ -632,12 +640,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
}
}
"""
- ).indented(),
- *stubs
+ ).indented(),
+ *stubs
)
- .run()
- .expect(
- """
+ .run()
+ .expect(
+ """
src/test/pkg/TestClass1.java:8: Warning: You cleared the original identity \
with Binder.clearCallingIdentity() and returned into token, so \
getCallingPid() will be using your own identity instead of the \
@@ -736,13 +744,58 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 12 warnings
""".addLineContinuation()
- )
+ )
+ }
+
+ /** Result of Binder.clearCallingIdentity() is not stored in a variable issue tests */
+
+ fun testDetectsResultOfClearIdentityCallNotStoredInVariable() {
+ lint().files(
+ java(
+ """
+ package test.pkg;
+ import android.os.Binder;
+ public class TestClass1 extends Binder {
+ private void testMethod() {
+ Binder.clearCallingIdentity();
+ android.os.Binder.clearCallingIdentity();
+ clearCallingIdentity();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass1.java:5: Warning: You cleared the original identity \
+ with Binder.clearCallingIdentity() but did not store the result in a \
+ variable. You need to store the result in a variable and restore it later. \
+ [ResultOfClearIdentityCallNotStoredInVariable]
+ Binder.clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:6: Warning: You cleared the original identity \
+ with android.os.Binder.clearCallingIdentity() but did not store the result \
+ in a variable. You need to store the result in a variable and restore it \
+ later. [ResultOfClearIdentityCallNotStoredInVariable]
+ android.os.Binder.clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:7: Warning: You cleared the original identity \
+ with clearCallingIdentity() but did not store the result in a variable. \
+ You need to store the result in a variable and restore it later. \
+ [ResultOfClearIdentityCallNotStoredInVariable]
+ clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 3 warnings
+ """.addLineContinuation()
+ )
}
/** Stubs for classes used for testing */
private val binderStub: TestFile = java(
- """
+ """
package android.os;
public class Binder {
public static final native long clearCallingIdentity() {
@@ -767,7 +820,7 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
).indented()
private val userHandleStub: TestFile = java(
- """
+ """
package android.os;
import android.annotation.AppIdInt;
import android.annotation.UserIdInt;
@@ -792,7 +845,7 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
).indented()
private val userIdIntStub: TestFile = java(
- """
+ """
package android.annotation;
public @interface UserIdInt {
}
@@ -800,7 +853,7 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
).indented()
private val appIdIntStub: TestFile = java(
- """
+ """
package android.annotation;
public @interface AppIdInt {
}
diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt
index e72f38416310..e72f38416310 100644
--- a/tools/lint/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt
+++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt
diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt
new file mode 100644
index 000000000000..a70644ab8532
--- /dev/null
+++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class PackageVisibilityDetectorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = PackageVisibilityDetector()
+
+ override fun getIssues(): MutableList<Issue> = mutableListOf(
+ PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS
+ )
+
+ override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+ fun testDetectIssuesParameterDoesNotApplyPackageVisibilityFilters() {
+ lint().files(java(
+ """
+ package com.android.server.lint.test;
+ import android.internal.test.IFoo;
+
+ public class TestClass extends IFoo.Stub {
+ @Override
+ public boolean hasPackage(String packageName) {
+ return packageName != null;
+ }
+ }
+ """).indented(), *stubs
+ ).run().expect(
+ """
+ src/com/android/server/lint/test/TestClass.java:6: Warning: \
+ Api: hasPackage contains a package name parameter: packageName does not apply \
+ package visibility filtering rules. \
+ [ApiMightLeakAppVisibility]
+ public boolean hasPackage(String packageName) {
+ ~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testDoesNotDetectIssuesApiInvokesAppOps() {
+ lint().files(java(
+ """
+ package com.android.server.lint.test;
+ import android.app.AppOpsManager;
+ import android.os.Binder;
+ import android.internal.test.IFoo;
+
+ public class TestClass extends IFoo.Stub {
+ private AppOpsManager mAppOpsManager;
+
+ @Override
+ public boolean hasPackage(String packageName) {
+ checkPackage(packageName);
+ return packageName != null;
+ }
+
+ private void checkPackage(String packageName) {
+ mAppOpsManager.checkPackage(Binder.getCallingUid(), packageName);
+ }
+ }
+ """
+ ).indented(), *stubs).run().expectClean()
+ }
+
+ fun testDoesNotDetectIssuesApiInvokesEnforcePermission() {
+ lint().files(java(
+ """
+ package com.android.server.lint.test;
+ import android.content.Context;
+ import android.internal.test.IFoo;
+
+ public class TestClass extends IFoo.Stub {
+ private Context mContext;
+
+ @Override
+ public boolean hasPackage(String packageName) {
+ enforcePermission();
+ return packageName != null;
+ }
+
+ private void enforcePermission() {
+ mContext.checkCallingPermission(
+ android.Manifest.permission.ACCESS_INPUT_FLINGER);
+ }
+ }
+ """
+ ).indented(), *stubs).run().expectClean()
+ }
+
+ fun testDoesNotDetectIssuesApiInvokesPackageManager() {
+ lint().files(java(
+ """
+ package com.android.server.lint.test;
+ import android.content.pm.PackageInfo;
+ import android.content.pm.PackageManager;
+ import android.internal.test.IFoo;
+
+ public class TestClass extends IFoo.Stub {
+ private PackageManager mPackageManager;
+
+ @Override
+ public boolean hasPackage(String packageName) {
+ return getPackageInfo(packageName) != null;
+ }
+
+ private PackageInfo getPackageInfo(String packageName) {
+ try {
+ return mPackageManager.getPackageInfo(packageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+ }
+ """
+ ).indented(), *stubs).run().expectClean()
+ }
+
+ fun testDetectIssuesApiInvokesPackageManagerAndClearCallingIdentify() {
+ lint().files(java(
+ """
+ package com.android.server.lint.test;
+ import android.content.pm.PackageInfo;
+ import android.content.pm.PackageManager;
+ import android.internal.test.IFoo;import android.os.Binder;
+
+ public class TestClass extends IFoo.Stub {
+ private PackageManager mPackageManager;
+
+ @Override
+ public boolean hasPackage(String packageName) {
+ return getPackageInfo(packageName) != null;
+ }
+
+ private PackageInfo getPackageInfo(String packageName) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ try {
+ return mPackageManager.getPackageInfo(packageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ } finally{
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+ """).indented(), *stubs
+ ).run().expect(
+ """
+ src/com/android/server/lint/test/TestClass.java:10: Warning: \
+ Api: hasPackage contains a package name parameter: packageName does not apply \
+ package visibility filtering rules. \
+ [ApiMightLeakAppVisibility]
+ public boolean hasPackage(String packageName) {
+ ~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testDoesNotDetectIssuesApiNotSystemPackagePrefix() {
+ lint().files(java(
+ """
+ package com.test.not.system.prefix;
+ import android.internal.test.IFoo;
+
+ public class TestClass extends IFoo.Stub {
+ @Override
+ public boolean hasPackage(String packageName) {
+ return packageName != null;
+ }
+ }
+ """
+ ).indented(), *stubs).run().expectClean()
+ }
+
+ private val contextStub: TestFile = java(
+ """
+ package android.content;
+
+ public abstract class Context {
+ public abstract int checkCallingPermission(String permission);
+ }
+ """
+ ).indented()
+
+ private val appOpsManagerStub: TestFile = java(
+ """
+ package android.app;
+
+ public class AppOpsManager {
+ public void checkPackage(int uid, String packageName) {
+ }
+ }
+ """
+ ).indented()
+
+ private val packageManagerStub: TestFile = java(
+ """
+ package android.content.pm;
+ import android.content.pm.PackageInfo;
+
+ public abstract class PackageManager {
+ public static class NameNotFoundException extends AndroidException {
+ }
+
+ public abstract PackageInfo getPackageInfo(String packageName, int flags)
+ throws NameNotFoundException;
+ }
+ """
+ ).indented()
+
+ private val packageInfoStub: TestFile = java(
+ """
+ package android.content.pm;
+ public class PackageInfo {}
+ """
+ ).indented()
+
+ private val binderStub: TestFile = java(
+ """
+ package android.os;
+
+ public class Binder {
+ public static final native long clearCallingIdentity();
+ public static final native void restoreCallingIdentity(long token);
+ public static final native int getCallingUid();
+ }
+ """
+ ).indented()
+
+ private val interfaceIFooStub: TestFile = java(
+ """
+ package android.internal.test;
+ import android.os.Binder;
+
+ public interface IFoo {
+ boolean hasPackage(String packageName);
+ public abstract static class Stub extends Binder implements IFoo {
+ }
+ }
+ """
+ ).indented()
+
+ private val stubs = arrayOf(contextStub, appOpsManagerStub, packageManagerStub,
+ packageInfoStub, binderStub, interfaceIFooStub)
+
+ // Substitutes "backslash + new line" with an empty string to imitate line continuation
+ private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
+}
diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt
new file mode 100644
index 000000000000..e686695ca804
--- /dev/null
+++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt
@@ -0,0 +1,823 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.parcel
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.checks.infrastructure.TestMode
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class SaferParcelCheckerTest : LintDetectorTest() {
+ override fun getDetector(): Detector = SaferParcelChecker()
+
+ override fun getIssues(): List<Issue> = listOf(
+ SaferParcelChecker.ISSUE_UNSAFE_API_USAGE
+ )
+
+ override fun lint(): TestLintTask =
+ super.lint()
+ .allowMissingSdk(true)
+ // We don't do partial analysis in the platform
+ .skipTestModes(TestMode.PARTIAL)
+
+ /** Parcel Tests */
+
+ fun testParcelDetectUnsafeReadSerializable() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.os.Parcel;
+ import java.io.Serializable;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ Serializable ans = p.readSerializable();
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .expectIdenticalTestModeOutput(false)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readSerializable() \
+ API usage [UnsafeParcelApi]
+ Serializable ans = p.readSerializable();
+ ~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testParcelDoesNotDetectSafeReadSerializable() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.os.Parcel;
+ import java.io.Serializable;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ String ans = p.readSerializable(null, String.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testParcelDetectUnsafeReadArrayList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ ArrayList ans = p.readArrayList(null);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:6: Warning: Unsafe Parcel.readArrayList() API \
+ usage [UnsafeParcelApi]
+ ArrayList ans = p.readArrayList(null);
+ ~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testParcelDoesNotDetectSafeReadArrayList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ ArrayList<Intent> ans = p.readArrayList(null, Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testParcelDetectUnsafeReadList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+ import java.util.List;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ List<Intent> list = new ArrayList<Intent>();
+ p.readList(list, null);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readList() API usage \
+ [UnsafeParcelApi]
+ p.readList(list, null);
+ ~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testDParceloesNotDetectSafeReadList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+ import java.util.List;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ List<Intent> list = new ArrayList<Intent>();
+ p.readList(list, null, Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testParcelDetectUnsafeReadParcelable() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ Intent ans = p.readParcelable(null);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readParcelable() API \
+ usage [UnsafeParcelApi]
+ Intent ans = p.readParcelable(null);
+ ~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testParcelDoesNotDetectSafeReadParcelable() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ Intent ans = p.readParcelable(null, Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testParcelDetectUnsafeReadParcelableList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+ import java.util.List;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ List<Intent> list = new ArrayList<Intent>();
+ List<Intent> ans = p.readParcelableList(list, null);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readParcelableList() \
+ API usage [UnsafeParcelApi]
+ List<Intent> ans = p.readParcelableList(list, null);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testParcelDoesNotDetectSafeReadParcelableList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+ import java.util.List;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ List<Intent> list = new ArrayList<Intent>();
+ List<Intent> ans =
+ p.readParcelableList(list, null, Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testParcelDetectUnsafeReadSparseArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+ import android.util.SparseArray;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ SparseArray<Intent> ans = p.readSparseArray(null);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:8: Warning: Unsafe Parcel.readSparseArray() API\
+ usage [UnsafeParcelApi]
+ SparseArray<Intent> ans = p.readSparseArray(null);
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testParcelDoesNotDetectSafeReadSparseArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+ import android.util.SparseArray;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ SparseArray<Intent> ans =
+ p.readSparseArray(null, Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testParcelDetectUnsafeReadSArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ Intent[] ans = p.readArray(null);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readArray() API\
+ usage [UnsafeParcelApi]
+ Intent[] ans = p.readArray(null);
+ ~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testParcelDoesNotDetectSafeReadArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ Intent[] ans = p.readArray(null, Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testParcelDetectUnsafeReadParcelableSArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ Intent[] ans = p.readParcelableArray(null);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readParcelableArray() API\
+ usage [UnsafeParcelApi]
+ Intent[] ans = p.readParcelableArray(null);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testParcelDoesNotDetectSafeReadParcelableArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ Intent[] ans = p.readParcelableArray(null, Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ /** Bundle Tests */
+
+ fun testBundleDetectUnsafeGetParcelable() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ Intent ans = b.getParcelable("key");
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelable() API usage [UnsafeParcelApi]
+ Intent ans = b.getParcelable("key");
+ ~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testBundleDoesNotDetectSafeGetParcelable() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ Intent ans = b.getParcelable("key", Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testBundleDetectUnsafeGetParcelableArrayList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ ArrayList<Intent> ans = b.getParcelableArrayList("key");
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArrayList() API usage [UnsafeParcelApi]
+ ArrayList<Intent> ans = b.getParcelableArrayList("key");
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testBundleDoesNotDetectSafeGetParcelableArrayList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ ArrayList<Intent> ans = b.getParcelableArrayList("key", Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testBundleDetectUnsafeGetParcelableArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ Intent[] ans = b.getParcelableArray("key");
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArray() API usage [UnsafeParcelApi]
+ Intent[] ans = b.getParcelableArray("key");
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testBundleDoesNotDetectSafeGetParcelableArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ Intent[] ans = b.getParcelableArray("key", Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testBundleDetectUnsafeGetSparseParcelableArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ SparseArray<Intent> ans = b.getSparseParcelableArray("key");
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getSparseParcelableArray() API usage [UnsafeParcelApi]
+ SparseArray<Intent> ans = b.getSparseParcelableArray("key");
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testBundleDoesNotDetectSafeGetSparseParcelableArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ SparseArray<Intent> ans = b.getSparseParcelableArray("key", Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ /** Intent Tests */
+
+ fun testIntentDetectUnsafeGetParcelableExtra() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+
+ public class TestClass {
+ private TestClass(Intent i) {
+ Intent ans = i.getParcelableExtra("name");
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:6: Warning: Unsafe Intent.getParcelableExtra() API usage [UnsafeParcelApi]
+ Intent ans = i.getParcelableExtra("name");
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testIntentDoesNotDetectSafeGetParcelableExtra() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+
+ public class TestClass {
+ private TestClass(Intent i) {
+ Intent ans = i.getParcelableExtra("name", Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+
+ /** Stubs for classes used for testing */
+
+
+ private val includes =
+ arrayOf(
+ manifest().minSdk("33"),
+ java(
+ """
+ package android.os;
+ import java.util.ArrayList;
+ import java.util.List;
+ import java.util.Map;
+ import java.util.HashMap;
+
+ public final class Parcel {
+ // Deprecated
+ public Object[] readArray(ClassLoader loader) { return null; }
+ public ArrayList readArrayList(ClassLoader loader) { return null; }
+ public HashMap readHashMap(ClassLoader loader) { return null; }
+ public void readList(List outVal, ClassLoader loader) {}
+ public void readMap(Map outVal, ClassLoader loader) {}
+ public <T extends Parcelable> T readParcelable(ClassLoader loader) { return null; }
+ public Parcelable[] readParcelableArray(ClassLoader loader) { return null; }
+ public Parcelable.Creator<?> readParcelableCreator(ClassLoader loader) { return null; }
+ public <T extends Parcelable> List<T> readParcelableList(List<T> list, ClassLoader cl) { return null; }
+ public Serializable readSerializable() { return null; }
+ public <T> SparseArray<T> readSparseArray(ClassLoader loader) { return null; }
+
+ // Replacements
+ public <T> T[] readArray(ClassLoader loader, Class<T> clazz) { return null; }
+ public <T> ArrayList<T> readArrayList(ClassLoader loader, Class<? extends T> clazz) { return null; }
+ public <K, V> HashMap<K,V> readHashMap(ClassLoader loader, Class<? extends K> clazzKey, Class<? extends V> clazzValue) { return null; }
+ public <T> void readList(List<? super T> outVal, ClassLoader loader, Class<T> clazz) {}
+ public <K, V> void readMap(Map<? super K, ? super V> outVal, ClassLoader loader, Class<K> clazzKey, Class<V> clazzValue) {}
+ public <T> T readParcelable(ClassLoader loader, Class<T> clazz) { return null; }
+ public <T> T[] readParcelableArray(ClassLoader loader, Class<T> clazz) { return null; }
+ public <T> Parcelable.Creator<T> readParcelableCreator(ClassLoader loader, Class<T> clazz) { return null; }
+ public <T> List<T> readParcelableList(List<T> list, ClassLoader cl, Class<T> clazz) { return null; }
+ public <T> T readSerializable(ClassLoader loader, Class<T> clazz) { return null; }
+ public <T> SparseArray<T> readSparseArray(ClassLoader loader, Class<? extends T> clazz) { return null; }
+ }
+ """
+ ).indented(),
+ java(
+ """
+ package android.os;
+ import java.util.ArrayList;
+ import java.util.List;
+ import java.util.Map;
+ import java.util.HashMap;
+
+ public final class Bundle {
+ // Deprecated
+ public <T extends Parcelable> T getParcelable(String key) { return null; }
+ public <T extends Parcelable> ArrayList<T> getParcelableArrayList(String key) { return null; }
+ public Parcelable[] getParcelableArray(String key) { return null; }
+ public <T extends Parcelable> SparseArray<T> getSparseParcelableArray(String key) { return null; }
+
+ // Replacements
+ public <T> T getParcelable(String key, Class<T> clazz) { return null; }
+ public <T> ArrayList<T> getParcelableArrayList(String key, Class<? extends T> clazz) { return null; }
+ public <T> T[] getParcelableArray(String key, Class<T> clazz) { return null; }
+ public <T> SparseArray<T> getSparseParcelableArray(String key, Class<? extends T> clazz) { return null; }
+
+ }
+ """
+ ).indented(),
+ java(
+ """
+ package android.os;
+ public interface Parcelable {}
+ """
+ ).indented(),
+ java(
+ """
+ package android.content;
+ public class Intent implements Parcelable, Cloneable {
+ // Deprecated
+ public <T extends Parcelable> T getParcelableExtra(String name) { return null; }
+
+ // Replacements
+ public <T> T getParcelableExtra(String name, Class<T> clazz) { return null; }
+
+ }
+ """
+ ).indented(),
+ java(
+ """
+ package android.util;
+ public class SparseArray<E> implements Cloneable {}
+ """
+ ).indented(),
+ )
+
+ // Substitutes "backslash + new line" with an empty string to imitate line continuation
+ private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
+}
diff --git a/tools/lint/global/Android.bp b/tools/lint/global/Android.bp
new file mode 100644
index 000000000000..bedb7bd78a29
--- /dev/null
+++ b/tools/lint/global/Android.bp
@@ -0,0 +1,65 @@
+// Copyright (C) 2022 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library_host {
+ name: "AndroidGlobalLintChecker",
+ srcs: ["checks/src/main/java/**/*.kt"],
+ plugins: ["auto_service_plugin"],
+ libs: [
+ "auto_service_annotations",
+ "lint_api",
+ ],
+ static_libs: ["AndroidCommonLint"],
+ kotlincflags: ["-Xjvm-default=all"],
+ dist: {
+ targets: ["droid"],
+ },
+}
+
+java_test_host {
+ name: "AndroidGlobalLintCheckerTest",
+ srcs: ["checks/src/test/java/**/*.kt"],
+ static_libs: [
+ "AndroidGlobalLintChecker",
+ "junit",
+ "lint",
+ "lint_tests",
+ ],
+ test_options: {
+ unit_test: true,
+ tradefed_options: [
+ {
+ // lint bundles in some classes that were built with older versions
+ // of libraries, and no longer load. Since tradefed tries to load
+ // all classes in the jar to look for tests, it crashes loading them.
+ // Exclude these classes from tradefed's search.
+ name: "exclude-paths",
+ value: "org/apache",
+ },
+ {
+ name: "exclude-paths",
+ value: "META-INF",
+ },
+ ],
+ },
+}
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt
index a6fd9bba6192..a20266a9b140 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2022 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.
@@ -19,21 +19,19 @@ package com.google.android.lint
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.CURRENT_API
+import com.google.android.lint.aidl.EnforcePermissionDetector
+import com.google.android.lint.aidl.EnforcePermissionHelperDetector
+import com.google.android.lint.aidl.SimpleManualPermissionEnforcementDetector
import com.google.auto.service.AutoService
@AutoService(IssueRegistry::class)
@Suppress("UnstableApiUsage")
-class AndroidFrameworkIssueRegistry : IssueRegistry() {
+class AndroidGlobalIssueRegistry : IssueRegistry() {
override val issues = listOf(
- CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN,
- CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN,
- CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
- CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
- CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
- CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
- CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED,
EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION,
- EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION
+ EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION,
+ EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER,
+ SimpleManualPermissionEnforcementDetector.ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT,
)
override val api: Int
@@ -45,6 +43,6 @@ class AndroidFrameworkIssueRegistry : IssueRegistry() {
override val vendor: Vendor = Vendor(
vendorName = "Android",
feedbackUrl = "http://b/issues/new?component=315013",
- contact = "brufino@google.com"
+ contact = "repsonsible-apis@google.com"
)
-}
+} \ No newline at end of file
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt
new file mode 100644
index 000000000000..ab6d871d6ea6
--- /dev/null
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.aidl
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+
+/**
+ * Abstract class for detectors that look for methods implementing
+ * generated AIDL interface stubs
+ */
+abstract class AidlImplementationDetector : Detector(), SourceCodeScanner {
+ override fun getApplicableUastTypes(): List<Class<out UElement?>> =
+ listOf(UMethod::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context)
+
+ private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() {
+ override fun visitMethod(node: UMethod) {
+ val interfaceName = getContainingAidlInterface(context, node)
+ .takeUnless(EXCLUDED_CPP_INTERFACES::contains) ?: return
+ val body = (node.uastBody as? UBlockExpression) ?: return
+ visitAidlMethod(context, node, interfaceName, body)
+ }
+ }
+
+ abstract fun visitAidlMethod(
+ context: JavaContext,
+ node: UMethod,
+ interfaceName: String,
+ body: UBlockExpression,
+ )
+}
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt
new file mode 100644
index 000000000000..dcfbe953f955
--- /dev/null
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.aidl
+
+const val ANNOTATION_ENFORCE_PERMISSION = "android.annotation.EnforcePermission"
+const val ANNOTATION_REQUIRES_NO_PERMISSION = "android.annotation.RequiresNoPermission"
+const val ANNOTATION_PERMISSION_MANUALLY_ENFORCED = "android.annotation.PermissionManuallyEnforced"
+
+val AIDL_PERMISSION_ANNOTATIONS = listOf(
+ ANNOTATION_ENFORCE_PERMISSION,
+ ANNOTATION_REQUIRES_NO_PERMISSION,
+ ANNOTATION_PERMISSION_MANUALLY_ENFORCED
+)
+
+const val BINDER_CLASS = "android.os.Binder"
+const val IINTERFACE_INTERFACE = "android.os.IInterface"
+
+const val AIDL_PERMISSION_HELPER_SUFFIX = "_enforcePermission"
+
+/**
+ * If a non java (e.g. c++) backend is enabled, the @EnforcePermission
+ * annotation cannot be used. At time of writing, the mechanism
+ * is not implemented for non java backends.
+ * TODO: b/242564874 (have lint know which interfaces have the c++ backend enabled)
+ * rather than hard coding this list?
+ */
+val EXCLUDED_CPP_INTERFACES = listOf(
+ "AdbTransportType",
+ "FingerprintAndPairDevice",
+ "IAdbCallback",
+ "IAdbManager",
+ "PairDevice",
+ "IStatsBootstrapAtomService",
+ "StatsBootstrapAtom",
+ "StatsBootstrapAtomValue",
+ "FixedSizeArrayExample",
+ "PlaybackTrackMetadata",
+ "RecordTrackMetadata",
+ "SinkMetadata",
+ "SourceMetadata",
+ "IUpdateEngineStable",
+ "IUpdateEngineStableCallback",
+ "AudioCapabilities",
+ "ConfidenceLevel",
+ "ModelParameter",
+ "ModelParameterRange",
+ "Phrase",
+ "PhraseRecognitionEvent",
+ "PhraseRecognitionExtra",
+ "PhraseSoundModel",
+ "Properties",
+ "RecognitionConfig",
+ "RecognitionEvent",
+ "RecognitionMode",
+ "RecognitionStatus",
+ "SoundModel",
+ "SoundModelType",
+ "Status",
+ "IThermalService",
+ "IPowerManager",
+ "ITunerResourceManager"
+)
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt
new file mode 100644
index 000000000000..0baac2c7aacf
--- /dev/null
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.aidl
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.AnnotationInfo
+import com.android.tools.lint.detector.api.AnnotationOrigin
+import com.android.tools.lint.detector.api.AnnotationUsageInfo
+import com.android.tools.lint.detector.api.AnnotationUsageType
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.ConstantEvaluator
+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.PsiAnnotation
+import com.intellij.psi.PsiArrayInitializerMemberValue
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.toUElement
+
+/**
+ * Lint Detector that ensures that any method overriding a method annotated
+ * with @EnforcePermission is also annotated with the exact same annotation.
+ * The intent is to surface the effective permission checks to the service
+ * implementations.
+ *
+ * This is done with 2 mechanisms:
+ * 1. Visit any annotation usage, to ensure that any derived class will have
+ * the correct annotation on each methods. This is for the top to bottom
+ * propagation.
+ * 2. Visit any annotation, to ensure that if a method is annotated, it has
+ * its ancestor also annotated. This is to avoid having an annotation on a
+ * Java method without the corresponding annotation on the AIDL interface.
+ */
+class EnforcePermissionDetector : Detector(), SourceCodeScanner {
+
+ override fun applicableAnnotations(): List<String> {
+ return listOf(ANNOTATION_ENFORCE_PERMISSION)
+ }
+
+ override fun getApplicableUastTypes(): List<Class<out UElement>> {
+ return listOf(UAnnotation::class.java)
+ }
+
+ private fun annotationValueGetChildren(elem: PsiElement): Array<PsiElement> {
+ if (elem is PsiArrayInitializerMemberValue)
+ return elem.getInitializers().map { it as PsiElement }.toTypedArray()
+ return elem.getChildren()
+ }
+
+ private fun areAnnotationsEquivalent(
+ context: JavaContext,
+ anno1: PsiAnnotation,
+ anno2: PsiAnnotation
+ ): Boolean {
+ if (anno1.qualifiedName != anno2.qualifiedName) {
+ return false
+ }
+ val attr1 = anno1.parameterList.attributes
+ val attr2 = anno2.parameterList.attributes
+ if (attr1.size != attr2.size) {
+ return false
+ }
+ for (i in attr1.indices) {
+ if (attr1[i].name != attr2[i].name) {
+ return false
+ }
+ val value1 = attr1[i].value ?: return false
+ val value2 = attr2[i].value ?: return false
+ // Try to compare values directly with each other.
+ val v1 = ConstantEvaluator.evaluate(context, value1)
+ val v2 = ConstantEvaluator.evaluate(context, value2)
+ if (v1 != null && v2 != null) {
+ if (v1 != v2) {
+ return false
+ }
+ } else {
+ val children1 = annotationValueGetChildren(value1)
+ val children2 = annotationValueGetChildren(value2)
+ if (children1.size != children2.size) {
+ return false
+ }
+ for (j in children1.indices) {
+ val c1 = ConstantEvaluator.evaluate(context, children1[j])
+ val c2 = ConstantEvaluator.evaluate(context, children2[j])
+ if (c1 != c2) {
+ return false
+ }
+ }
+ }
+ }
+ return true
+ }
+
+ private fun compareMethods(
+ context: JavaContext,
+ element: UElement,
+ overridingMethod: PsiMethod,
+ overriddenMethod: PsiMethod,
+ checkEquivalence: Boolean = true
+ ) {
+ // If method is not from a Stub subclass, this method shouldn't use @EP at all.
+ // This is handled by EnforcePermissionHelperDetector.
+ if (!isContainedInSubclassOfStub(context, overridingMethod.toUElement() as? UMethod)) {
+ return
+ }
+ val overridingAnnotation = overridingMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION)
+ val overriddenAnnotation = overriddenMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION)
+ val location = context.getLocation(element)
+ val overridingClass = overridingMethod.parent as PsiClass
+ val overriddenClass = overriddenMethod.parent as PsiClass
+ val overridingName = "${overridingClass.name}.${overridingMethod.name}"
+ val overriddenName = "${overriddenClass.name}.${overriddenMethod.name}"
+ if (overridingAnnotation == null) {
+ val msg = "The method $overridingName overrides the method $overriddenName which " +
+ "is annotated with @EnforcePermission. The same annotation must be used " +
+ "on $overridingName"
+ context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
+ } else if (overriddenAnnotation == null) {
+ val msg = "The method $overridingName overrides the method $overriddenName which " +
+ "is not annotated with @EnforcePermission. The same annotation must be " +
+ "used on $overriddenName. Did you forget to annotate the AIDL definition?"
+ context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
+ } else if (checkEquivalence && !areAnnotationsEquivalent(
+ context, overridingAnnotation, overriddenAnnotation)) {
+ val msg = "The method $overridingName is annotated with " +
+ "${overridingAnnotation.text} which differs from the overridden " +
+ "method $overriddenName: ${overriddenAnnotation.text}. The same " +
+ "annotation must be used for both methods."
+ context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg)
+ }
+ }
+
+ override fun visitAnnotationUsage(
+ context: JavaContext,
+ element: UElement,
+ annotationInfo: AnnotationInfo,
+ usageInfo: AnnotationUsageInfo
+ ) {
+ if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE &&
+ annotationInfo.origin == AnnotationOrigin.METHOD) {
+ val overridingMethod = element.sourcePsi as PsiMethod
+ val overriddenMethod = usageInfo.referenced as PsiMethod
+ compareMethods(context, element, overridingMethod, overriddenMethod)
+ }
+ }
+
+ override fun createUastHandler(context: JavaContext): UElementHandler {
+ return object : UElementHandler() {
+ override fun visitAnnotation(node: UAnnotation) {
+ if (node.qualifiedName != ANNOTATION_ENFORCE_PERMISSION) {
+ return
+ }
+ val method = node.uastParent as? UMethod ?: return
+ val overridingMethod = method as PsiMethod
+ val parents = overridingMethod.findSuperMethods()
+ for (overriddenMethod in parents) {
+ // The equivalence check can be skipped, if both methods are
+ // annotated, it will be verified by visitAnnotationUsage.
+ compareMethods(context, method, overridingMethod,
+ overriddenMethod, checkEquivalence = false)
+ }
+ }
+ }
+ }
+
+ companion object {
+ val EXPLANATION = """
+ The @EnforcePermission annotation is used to indicate that the underlying binder code
+ has already verified the caller's permissions before calling the appropriate method. The
+ verification code is usually generated by the AIDL compiler, which also takes care of
+ annotating the generated Java code.
+
+ In order to surface that information to platform developers, the same annotation must be
+ used on the implementation class or methods.
+ """
+
+ val ISSUE_MISSING_ENFORCE_PERMISSION: Issue = Issue.create(
+ id = "MissingEnforcePermissionAnnotation",
+ briefDescription = "Missing @EnforcePermission annotation on Binder method",
+ explanation = EXPLANATION,
+ category = Category.SECURITY,
+ priority = 6,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ EnforcePermissionDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+
+ val ISSUE_MISMATCHING_ENFORCE_PERMISSION: Issue = Issue.create(
+ id = "MismatchingEnforcePermissionAnnotation",
+ briefDescription = "Incorrect @EnforcePermission annotation on Binder method",
+ explanation = EXPLANATION,
+ category = Category.SECURITY,
+ priority = 6,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ EnforcePermissionDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+ }
+}
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
new file mode 100644
index 000000000000..25d208db14ec
--- /dev/null
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.aidl
+
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.LintFix
+import com.android.tools.lint.detector.api.Location
+import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationBooleanValue
+import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationStringValues
+import com.android.tools.lint.detector.api.findSelector
+import com.android.tools.lint.detector.api.getUMethod
+import com.google.android.lint.findCallExpression
+import com.google.android.lint.getPermissionMethodAnnotation
+import com.google.android.lint.hasPermissionNameAnnotation
+import com.google.android.lint.isPermissionMethodCall
+import com.intellij.psi.PsiClassType
+import com.intellij.psi.PsiType
+import org.jetbrains.kotlin.psi.psiUtil.parameterIndex
+import org.jetbrains.uast.UBinaryExpression
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UExpressionList
+import org.jetbrains.uast.UIfExpression
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UThrowExpression
+import org.jetbrains.uast.UastBinaryOperator
+import org.jetbrains.uast.evaluateString
+import org.jetbrains.uast.skipParenthesizedExprDown
+import org.jetbrains.uast.visitor.AbstractUastVisitor
+
+/**
+ * Helper class that facilitates the creation of lint auto fixes
+ */
+data class EnforcePermissionFix(
+ val manualCheckLocations: List<Location>,
+ val permissionNames: List<String>,
+ val errorLevel: Boolean,
+ val anyOf: Boolean,
+) {
+ fun toLintFix(context: JavaContext, node: UMethod): LintFix {
+ val methodLocation = context.getLocation(node)
+ val replaceOrRemoveFixes = manualCheckLocations.mapIndexed { index, manualCheckLocation ->
+ if (index == 0) {
+ // Replace the first manual check with a call to the helper method
+ getHelperMethodFix(node, manualCheckLocation, false)
+ } else {
+ // Remove all subsequent manual checks
+ LintFix.create()
+ .replace()
+ .reformat(true)
+ .range(manualCheckLocation)
+ .with("")
+ .autoFix()
+ .build()
+ }
+ }
+
+ // Annotate the method with @EnforcePermission(...)
+ val annotateFix = LintFix.create()
+ .annotate(annotation)
+ .range(methodLocation)
+ .autoFix()
+ .build()
+
+ return LintFix.create().composite(annotateFix, *replaceOrRemoveFixes.toTypedArray())
+ }
+
+ private val annotation: String
+ get() {
+ val quotedPermissions = permissionNames.joinToString(", ") { """"$it"""" }
+
+ val attributeName =
+ if (permissionNames.size > 1) {
+ if (anyOf) "anyOf" else "allOf"
+ } else null
+
+ val annotationParameter =
+ if (attributeName != null) "$attributeName={$quotedPermissions}"
+ else quotedPermissions
+
+ return "@$ANNOTATION_ENFORCE_PERMISSION($annotationParameter)"
+ }
+
+ companion object {
+ /**
+ * Walks the expressions in a block, looking for simple permission checks.
+ *
+ * As soon as something other than a permission check is encountered, stop looking,
+ * as some other business logic is happening that prevents an automated fix.
+ */
+ fun fromBlockExpression(
+ context: JavaContext,
+ blockExpression: UBlockExpression
+ ): EnforcePermissionFix? {
+ try {
+ val singleFixes = mutableListOf<EnforcePermissionFix>()
+ for (expression in blockExpression.expressions) {
+ val fix = fromExpression(context, expression) ?: break
+ singleFixes.add(fix)
+ }
+ return compose(singleFixes)
+ } catch (e: AnyOfAllOfException) {
+ return null
+ }
+ }
+
+ /**
+ * Conditionally constructs EnforcePermissionFix from any UExpression
+ *
+ * @return EnforcePermissionFix if the expression boils down to a permission check,
+ * else null
+ */
+ fun fromExpression(
+ context: JavaContext,
+ expression: UExpression
+ ): EnforcePermissionFix? {
+ val trimmedExpression = expression.skipParenthesizedExprDown()
+ if (trimmedExpression is UIfExpression) {
+ return fromIfExpression(context, trimmedExpression)
+ }
+ findCallExpression(trimmedExpression)?.let {
+ return fromCallExpression(context, it)
+ }
+ return null
+ }
+
+ /**
+ * Conditionally constructs EnforcePermissionFix from a UCallExpression
+ *
+ * @return EnforcePermissionFix if the called method is annotated with @PermissionMethod, else null
+ */
+ fun fromCallExpression(
+ context: JavaContext,
+ callExpression: UCallExpression
+ ): EnforcePermissionFix? {
+ val method = callExpression.resolve()?.getUMethod() ?: return null
+ val annotation = getPermissionMethodAnnotation(method) ?: return null
+ val returnsVoid = method.returnType == PsiType.VOID
+ val orSelf = getAnnotationBooleanValue(annotation, "orSelf") ?: false
+ val anyOf = getAnnotationBooleanValue(annotation, "anyOf") ?: false
+ return EnforcePermissionFix(
+ listOf(getPermissionCheckLocation(context, callExpression)),
+ getPermissionCheckValues(callExpression),
+ errorLevel = isErrorLevel(throws = returnsVoid, orSelf = orSelf),
+ anyOf,
+ )
+ }
+
+ /**
+ * Conditionally constructs EnforcePermissionFix from a UCallExpression
+ *
+ * @return EnforcePermissionFix IF AND ONLY IF:
+ * * The condition of the if statement compares the return value of a
+ * PermissionMethod to one of the PackageManager.PermissionResult values
+ * * The expression inside the if statement does nothing but throw SecurityException
+ */
+ fun fromIfExpression(
+ context: JavaContext,
+ ifExpression: UIfExpression
+ ): EnforcePermissionFix? {
+ val condition = ifExpression.condition.skipParenthesizedExprDown()
+ if (condition !is UBinaryExpression) return null
+
+ val maybeLeftCall = findCallExpression(condition.leftOperand)
+ val maybeRightCall = findCallExpression(condition.rightOperand)
+
+ val (callExpression, comparison) =
+ if (maybeLeftCall is UCallExpression) {
+ Pair(maybeLeftCall, condition.rightOperand)
+ } else if (maybeRightCall is UCallExpression) {
+ Pair(maybeRightCall, condition.leftOperand)
+ } else return null
+
+ val permissionMethodAnnotation = getPermissionMethodAnnotation(
+ callExpression.resolve()?.getUMethod()) ?: return null
+
+ val equalityCheck =
+ when (comparison.findSelector().asSourceString()
+ .filterNot(Char::isWhitespace)) {
+ "PERMISSION_GRANTED" -> UastBinaryOperator.IDENTITY_NOT_EQUALS
+ "PERMISSION_DENIED" -> UastBinaryOperator.IDENTITY_EQUALS
+ else -> return null
+ }
+
+ if (condition.operator != equalityCheck) return null
+
+ val throwExpression: UThrowExpression? =
+ ifExpression.thenExpression as? UThrowExpression
+ ?: (ifExpression.thenExpression as? UBlockExpression)
+ ?.expressions?.firstOrNull()
+ as? UThrowExpression
+
+
+ val thrownClass = (throwExpression?.thrownExpression?.getExpressionType()
+ as? PsiClassType)?.resolve() ?: return null
+ if (!context.evaluator.inheritsFrom(
+ thrownClass, "java.lang.SecurityException")){
+ return null
+ }
+
+ val orSelf = getAnnotationBooleanValue(permissionMethodAnnotation, "orSelf") ?: false
+ val anyOf = getAnnotationBooleanValue(permissionMethodAnnotation, "anyOf") ?: false
+
+ return EnforcePermissionFix(
+ listOf(context.getLocation(ifExpression)),
+ getPermissionCheckValues(callExpression),
+ errorLevel = isErrorLevel(throws = true, orSelf = orSelf),
+ anyOf = anyOf
+ )
+ }
+
+
+ fun compose(individuals: List<EnforcePermissionFix>): EnforcePermissionFix? {
+ if (individuals.isEmpty()) return null
+ val anyOfs = individuals.filter(EnforcePermissionFix::anyOf)
+ // anyOf/allOf should be consistent. If we encounter some @PermissionMethods that are anyOf
+ // and others that aren't, we don't know what to do.
+ if (anyOfs.isNotEmpty() && anyOfs.size < individuals.size) {
+ throw AnyOfAllOfException()
+ }
+ return EnforcePermissionFix(
+ individuals.flatMap(EnforcePermissionFix::manualCheckLocations),
+ individuals.flatMap(EnforcePermissionFix::permissionNames),
+ errorLevel = individuals.all(EnforcePermissionFix::errorLevel),
+ anyOf = anyOfs.isNotEmpty()
+ )
+ }
+
+ /**
+ * Given a permission check, get its proper location
+ * so that a lint fix can remove the entire expression
+ */
+ private fun getPermissionCheckLocation(
+ context: JavaContext,
+ callExpression: UCallExpression
+ ):
+ Location {
+ val javaPsi = callExpression.javaPsi!!
+ return Location.create(
+ context.file,
+ javaPsi.containingFile?.text,
+ javaPsi.textRange.startOffset,
+ // unfortunately the element doesn't include the ending semicolon
+ javaPsi.textRange.endOffset + 1
+ )
+ }
+
+ /**
+ * Given a @PermissionMethod, find arguments annotated with @PermissionName
+ * and pull out the permission value(s) being used. Also evaluates nested calls
+ * to @PermissionMethod(s) in the given method's body.
+ */
+ @Throws(AnyOfAllOfException::class)
+ private fun getPermissionCheckValues(
+ callExpression: UCallExpression
+ ): List<String> {
+ if (!isPermissionMethodCall(callExpression)) return emptyList()
+
+ val result = mutableSetOf<String>() // protect against duplicate permission values
+ val visitedCalls = mutableSetOf<UCallExpression>() // don't visit the same call twice
+ val bfsQueue = ArrayDeque(listOf(callExpression))
+
+ var anyOfAllOfState: AnyOfAllOfState = AnyOfAllOfState.INITIAL
+
+ // Bread First Search - evaluating nested @PermissionMethod(s) in the available
+ // source code for @PermissionName(s).
+ while (bfsQueue.isNotEmpty()) {
+ val currentCallExpression = bfsQueue.removeFirst()
+ visitedCalls.add(currentCallExpression)
+ val currentPermissions = findPermissions(currentCallExpression)
+ result.addAll(currentPermissions)
+
+ val currentAnnotation = getPermissionMethodAnnotation(
+ currentCallExpression.resolve()?.getUMethod())
+ val currentAnyOf = getAnnotationBooleanValue(currentAnnotation, "anyOf") ?: false
+
+ // anyOf/allOf should be consistent. If we encounter a nesting of @PermissionMethods
+ // where we start in an anyOf state and switch to allOf, or vice versa,
+ // we don't know what to do.
+ if (anyOfAllOfState == AnyOfAllOfState.INITIAL) {
+ if (currentAnyOf) anyOfAllOfState = AnyOfAllOfState.ANY_OF
+ else if (result.isNotEmpty()) anyOfAllOfState = AnyOfAllOfState.ALL_OF
+ }
+
+ if (anyOfAllOfState == AnyOfAllOfState.ALL_OF && currentAnyOf) {
+ throw AnyOfAllOfException()
+ }
+
+ if (anyOfAllOfState == AnyOfAllOfState.ANY_OF &&
+ !currentAnyOf && currentPermissions.size > 1) {
+ throw AnyOfAllOfException()
+ }
+
+ currentCallExpression.resolve()?.getUMethod()
+ ?.accept(PermissionCheckValuesVisitor(visitedCalls, bfsQueue))
+ }
+
+ return result.toList()
+ }
+
+ private enum class AnyOfAllOfState {
+ INITIAL,
+ ANY_OF,
+ ALL_OF
+ }
+
+ /**
+ * Adds visited permission method calls to the provided
+ * queue in support of the BFS traversal happening while
+ * this is used
+ */
+ private class PermissionCheckValuesVisitor(
+ val visitedCalls: Set<UCallExpression>,
+ val bfsQueue: ArrayDeque<UCallExpression>
+ ) : AbstractUastVisitor() {
+ override fun visitCallExpression(node: UCallExpression): Boolean {
+ if (isPermissionMethodCall(node) && node !in visitedCalls) {
+ bfsQueue.add(node)
+ }
+ return false
+ }
+ }
+
+ private fun findPermissions(
+ callExpression: UCallExpression,
+ ): List<String> {
+ val annotation = getPermissionMethodAnnotation(callExpression.resolve()?.getUMethod())
+
+ val hardCodedPermissions = (getAnnotationStringValues(annotation, "value")
+ ?: emptyArray())
+ .toList()
+
+ val indices = callExpression.resolve()?.getUMethod()
+ ?.uastParameters
+ ?.filter(::hasPermissionNameAnnotation)
+ ?.mapNotNull { it.sourcePsi?.parameterIndex() }
+ ?: emptyList()
+
+ val argPermissions = indices
+ .flatMap { i ->
+ when (val argument = callExpression.getArgumentForParameter(i)) {
+ null -> listOf(null)
+ is UExpressionList -> // varargs e.g. someMethod(String...)
+ argument.expressions.map(UExpression::evaluateString)
+ else -> listOf(argument.evaluateString())
+ }
+ }
+ .filterNotNull()
+
+ return hardCodedPermissions + argPermissions
+ }
+
+ /**
+ * If we detect that the PermissionMethod enforces that permission is granted,
+ * AND is of the "orSelf" variety, we are very confident that this is a behavior
+ * preserving migration to @EnforcePermission. Thus, the incident should be ERROR
+ * level.
+ */
+ private fun isErrorLevel(throws: Boolean, orSelf: Boolean): Boolean = throws && orSelf
+ }
+}
+/**
+ * anyOf/allOf @PermissionMethods must be consistent to apply @EnforcePermission -
+ * meaning if we encounter some @PermissionMethods that are anyOf, and others are allOf,
+ * we don't know which to apply.
+ */
+class AnyOfAllOfException : Exception() {
+ override val message: String = "anyOf/allOf permission methods cannot be mixed"
+}
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt
new file mode 100644
index 000000000000..df13af516514
--- /dev/null
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.aidl
+
+import com.android.tools.lint.client.api.UElementHandler
+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.google.android.lint.findCallExpression
+import com.intellij.psi.PsiElement
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UDeclarationsExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.skipParenthesizedExprDown
+
+class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner {
+ override fun getApplicableUastTypes(): List<Class<out UElement?>> =
+ listOf(UMethod::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context)
+
+ private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() {
+ override fun visitMethod(node: UMethod) {
+ if (context.evaluator.isAbstract(node)) return
+ if (!node.hasAnnotation(ANNOTATION_ENFORCE_PERMISSION)) return
+
+ if (!isContainedInSubclassOfStub(context, node)) {
+ context.report(
+ ISSUE_MISUSING_ENFORCE_PERMISSION,
+ node,
+ context.getLocation(node),
+ "The class of ${node.name} does not inherit from an AIDL generated Stub class"
+ )
+ return
+ }
+
+ val targetExpression = getHelperMethodCallSourceString(node)
+ val message =
+ "Method must start with $targetExpression or super.${node.name}(), if applicable"
+
+ val firstExpression = (node.uastBody as? UBlockExpression)
+ ?.expressions?.firstOrNull()
+
+ if (firstExpression == null) {
+ context.report(
+ ISSUE_ENFORCE_PERMISSION_HELPER,
+ context.getLocation(node),
+ message,
+ )
+ return
+ }
+
+ val firstExpressionSource = firstExpression.skipParenthesizedExprDown()
+ .asSourceString()
+ .filterNot(Char::isWhitespace)
+
+ if (firstExpressionSource != targetExpression &&
+ firstExpressionSource != "super.$targetExpression") {
+ // calling super.<methodName>() is also legal
+ val directSuper = context.evaluator.getSuperMethod(node)
+ val firstCall = findCallExpression(firstExpression)?.resolve()
+ if (directSuper != null && firstCall == directSuper) return
+
+ val locationTarget = getLocationTarget(firstExpression)
+ val expressionLocation = context.getLocation(locationTarget)
+
+ context.report(
+ ISSUE_ENFORCE_PERMISSION_HELPER,
+ context.getLocation(node),
+ message,
+ getHelperMethodFix(node, expressionLocation),
+ )
+ }
+ }
+ }
+
+ companion object {
+ private const val HELPER_SUFFIX = "_enforcePermission"
+
+ private const val EXPLANATION = """
+ The @EnforcePermission annotation can only be used on methods whose class extends from
+ the Stub class generated by the AIDL compiler. When @EnforcePermission is applied, the
+ AIDL compiler generates a Stub method to do the permission check called yourMethodName$HELPER_SUFFIX.
+
+ yourMethodName$HELPER_SUFFIX must be executed before any other operation. To do that, you can
+ either call it directly, or call it indirectly via super.yourMethodName().
+ """
+
+ val ISSUE_ENFORCE_PERMISSION_HELPER: Issue = Issue.create(
+ id = "MissingEnforcePermissionHelper",
+ briefDescription = """Missing permission-enforcing method call in AIDL method
+ |annotated with @EnforcePermission""".trimMargin(),
+ explanation = EXPLANATION,
+ category = Category.SECURITY,
+ priority = 6,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ EnforcePermissionHelperDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+
+ val ISSUE_MISUSING_ENFORCE_PERMISSION: Issue = Issue.create(
+ id = "MisusingEnforcePermissionAnnotation",
+ briefDescription = "@EnforcePermission cannot be used here",
+ explanation = EXPLANATION,
+ category = Category.SECURITY,
+ priority = 6,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ EnforcePermissionDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+
+ /**
+ * handles an edge case with UDeclarationsExpression, where sourcePsi is null,
+ * resulting in an incorrect Location if used directly
+ */
+ private fun getLocationTarget(firstExpression: UExpression): PsiElement? {
+ if (firstExpression.sourcePsi != null) return firstExpression.sourcePsi
+ if (firstExpression is UDeclarationsExpression) {
+ return firstExpression.declarations.firstOrNull()?.sourcePsi
+ }
+ return null
+ }
+ }
+}
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
new file mode 100644
index 000000000000..d41fee3fc0dc
--- /dev/null
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.aidl
+
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.LintFix
+import com.android.tools.lint.detector.api.Location
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiReferenceList
+import org.jetbrains.uast.UMethod
+
+/**
+ * Given a UMethod, determine if this method is
+ * the entrypoint to an interface generated by AIDL,
+ * returning the interface name if so, otherwise returning null
+ */
+fun getContainingAidlInterface(context: JavaContext, node: UMethod): String? {
+ if (!isContainedInSubclassOfStub(context, node)) return null
+ for (superMethod in node.findSuperMethods()) {
+ for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements
+ ?: continue) {
+ if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) {
+ return superMethod.containingClass?.name
+ }
+ }
+ }
+ return null
+}
+
+fun isContainedInSubclassOfStub(context: JavaContext, node: UMethod?): Boolean {
+ var superClass = node?.containingClass?.superClass
+ while (superClass != null) {
+ if (isStub(context, superClass)) return true
+ superClass = superClass.superClass
+ }
+ return false
+}
+
+fun isStub(context: JavaContext, psiClass: PsiClass?): Boolean {
+ if (psiClass == null) return false
+ if (psiClass.name != "Stub") return false
+ if (!context.evaluator.isStatic(psiClass)) return false
+ if (!context.evaluator.isAbstract(psiClass)) return false
+
+ if (!hasSingleAncestor(psiClass.extendsList, BINDER_CLASS)) return false
+
+ val parent = psiClass.parent as? PsiClass ?: return false
+ if (!hasSingleAncestor(parent.extendsList, IINTERFACE_INTERFACE)) return false
+
+ val parentName = parent.qualifiedName ?: return false
+ if (!hasSingleAncestor(psiClass.implementsList, parentName)) return false
+
+ return true
+}
+
+private fun hasSingleAncestor(references: PsiReferenceList?, qualifiedName: String) =
+ references != null &&
+ references.referenceElements.size == 1 &&
+ references.referenceElements[0].qualifiedName == qualifiedName
+
+fun getHelperMethodCallSourceString(node: UMethod) = "${node.name}$AIDL_PERMISSION_HELPER_SUFFIX()"
+
+fun getHelperMethodFix(
+ node: UMethod,
+ manualCheckLocation: Location,
+ prepend: Boolean = true
+): LintFix {
+ val helperMethodSource = getHelperMethodCallSourceString(node)
+ val indent = " ".repeat(manualCheckLocation.start?.column ?: 0)
+ val newText = "$helperMethodSource;${if (prepend) "\n\n$indent" else ""}"
+
+ val fix = LintFix.create()
+ .replace()
+ .range(manualCheckLocation)
+ .with(newText)
+ .reformat(true)
+ .autoFix()
+
+ if (prepend) fix.beginning()
+
+ return fix.build()
+}
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt
new file mode 100644
index 000000000000..c7be36efd991
--- /dev/null
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.aidl
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Incident
+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 org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UMethod
+
+/**
+ * Looks for methods implementing generated AIDL interface stubs
+ * that can have simple permission checks migrated to
+ * @EnforcePermission annotations
+ */
+@Suppress("UnstableApiUsage")
+class SimpleManualPermissionEnforcementDetector : AidlImplementationDetector() {
+ override fun visitAidlMethod(
+ context: JavaContext,
+ node: UMethod,
+ interfaceName: String,
+ body: UBlockExpression
+ ) {
+ val enforcePermissionFix = EnforcePermissionFix.fromBlockExpression(context, body) ?: return
+ val lintFix = enforcePermissionFix.toLintFix(context, node)
+ val message =
+ "$interfaceName permission check ${
+ if (enforcePermissionFix.errorLevel) "should" else "can"
+ } be converted to @EnforcePermission annotation"
+
+ val incident = Incident(
+ ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT,
+ enforcePermissionFix.manualCheckLocations.last(),
+ message,
+ lintFix
+ )
+
+ // TODO(b/265014041): turn on errors once all code that would cause one is fixed
+ // if (enforcePermissionFix.errorLevel) {
+ // incident.overrideSeverity(Severity.ERROR)
+ // }
+
+ context.report(incident)
+ }
+
+ companion object {
+
+ private val EXPLANATION = """
+ Whenever possible, method implementations of AIDL interfaces should use the @EnforcePermission
+ annotation to declare the permissions to be enforced. The verification code is then
+ generated by the AIDL compiler, which also takes care of annotating the generated java
+ code.
+
+ This reduces the risk of bugs around these permission checks (that often become vulnerabilities).
+ It also enables easier auditing and review.
+
+ Please migrate to an @EnforcePermission annotation. (See: go/aidl-enforce-howto)
+ """.trimIndent()
+
+ @JvmField
+ val ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT = Issue.create(
+ id = "SimpleManualPermissionEnforcement",
+ briefDescription = "Manual permission check can be @EnforcePermission annotation",
+ explanation = EXPLANATION,
+ category = Category.SECURITY,
+ priority = 5,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ SimpleManualPermissionEnforcementDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ ),
+ )
+ }
+}
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorCodegenTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorCodegenTest.kt
new file mode 100644
index 000000000000..f2930d9faac7
--- /dev/null
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorCodegenTest.kt
@@ -0,0 +1,557 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.aidl
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class EnforcePermissionDetectorCodegenTest : LintDetectorTest() {
+ override fun getDetector(): Detector = EnforcePermissionDetector()
+
+ override fun getIssues(): List<Issue> = listOf(
+ EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION,
+ EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION
+ )
+
+ override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+ fun test_generated_IProtected() {
+ lint().files(
+ java(
+ """
+ /*
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+ package android.aidl.tests.permission;
+ public interface IProtected extends android.os.IInterface
+ {
+ /** Default implementation for IProtected. */
+ public static class Default implements android.aidl.tests.permission.IProtected
+ {
+ @Override public void PermissionProtected() throws android.os.RemoteException
+ {
+ }
+ @Override public void MultiplePermissionsAll() throws android.os.RemoteException
+ {
+ }
+ @Override public void MultiplePermissionsAny() throws android.os.RemoteException
+ {
+ }
+ @Override public void NonManifestPermission() throws android.os.RemoteException
+ {
+ }
+ // Used by the integration tests to dynamically set permissions that are considered granted.
+ @Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException
+ {
+ }
+ @Override
+ public android.os.IBinder asBinder() {
+ return null;
+ }
+ }
+ /** Local-side IPC implementation stub class. */
+ public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtected
+ {
+ private final android.os.PermissionEnforcer mEnforcer;
+ /** Construct the stub using the Enforcer provided. */
+ public Stub(android.os.PermissionEnforcer enforcer)
+ {
+ this.attachInterface(this, DESCRIPTOR);
+ if (enforcer == null) {
+ throw new IllegalArgumentException("enforcer cannot be null");
+ }
+ mEnforcer = enforcer;
+ }
+ @Deprecated
+ /** Default constructor. */
+ public Stub() {
+ this(android.os.PermissionEnforcer.fromContext(
+ android.app.ActivityThread.currentActivityThread().getSystemContext()));
+ }
+ /**
+ * Cast an IBinder object into an android.aidl.tests.permission.IProtected interface,
+ * generating a proxy if needed.
+ */
+ public static android.aidl.tests.permission.IProtected asInterface(android.os.IBinder obj)
+ {
+ if ((obj==null)) {
+ return null;
+ }
+ android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+ if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtected))) {
+ return ((android.aidl.tests.permission.IProtected)iin);
+ }
+ return new android.aidl.tests.permission.IProtected.Stub.Proxy(obj);
+ }
+ @Override public android.os.IBinder asBinder()
+ {
+ return this;
+ }
+ /** @hide */
+ public static java.lang.String getDefaultTransactionName(int transactionCode)
+ {
+ switch (transactionCode)
+ {
+ case TRANSACTION_PermissionProtected:
+ {
+ return "PermissionProtected";
+ }
+ case TRANSACTION_MultiplePermissionsAll:
+ {
+ return "MultiplePermissionsAll";
+ }
+ case TRANSACTION_MultiplePermissionsAny:
+ {
+ return "MultiplePermissionsAny";
+ }
+ case TRANSACTION_NonManifestPermission:
+ {
+ return "NonManifestPermission";
+ }
+ case TRANSACTION_SetGranted:
+ {
+ return "SetGranted";
+ }
+ default:
+ {
+ return null;
+ }
+ }
+ }
+ /** @hide */
+ public java.lang.String getTransactionName(int transactionCode)
+ {
+ return this.getDefaultTransactionName(transactionCode);
+ }
+ @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
+ {
+ java.lang.String descriptor = DESCRIPTOR;
+ if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
+ data.enforceInterface(descriptor);
+ }
+ switch (code)
+ {
+ case INTERFACE_TRANSACTION:
+ {
+ reply.writeString(descriptor);
+ return true;
+ }
+ }
+ switch (code)
+ {
+ case TRANSACTION_PermissionProtected:
+ {
+ this.PermissionProtected();
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_MultiplePermissionsAll:
+ {
+ this.MultiplePermissionsAll();
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_MultiplePermissionsAny:
+ {
+ this.MultiplePermissionsAny();
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_NonManifestPermission:
+ {
+ this.NonManifestPermission();
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_SetGranted:
+ {
+ java.util.List<java.lang.String> _arg0;
+ _arg0 = data.createStringArrayList();
+ data.enforceNoDataAvail();
+ this.SetGranted(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ default:
+ {
+ return super.onTransact(code, data, reply, flags);
+ }
+ }
+ return true;
+ }
+ private static class Proxy implements android.aidl.tests.permission.IProtected
+ {
+ private android.os.IBinder mRemote;
+ Proxy(android.os.IBinder remote)
+ {
+ mRemote = remote;
+ }
+ @Override public android.os.IBinder asBinder()
+ {
+ return mRemote;
+ }
+ public java.lang.String getInterfaceDescriptor()
+ {
+ return DESCRIPTOR;
+ }
+ @Override public void PermissionProtected() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_PermissionProtected, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void MultiplePermissionsAll() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAll, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void MultiplePermissionsAny() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAny, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void NonManifestPermission() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_NonManifestPermission, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ // Used by the integration tests to dynamically set permissions that are considered granted.
+ @Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeStringList(permissions);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_SetGranted, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ }
+ static final int TRANSACTION_PermissionProtected = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+ /** Helper method to enforce permissions for PermissionProtected */
+ protected void PermissionProtected_enforcePermission() throws SecurityException {
+ android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
+ mEnforcer.enforcePermission(android.Manifest.permission.READ_PHONE_STATE, source);
+ }
+ static final int TRANSACTION_MultiplePermissionsAll = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+ /** Helper method to enforce permissions for MultiplePermissionsAll */
+ protected void MultiplePermissionsAll_enforcePermission() throws SecurityException {
+ android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
+ mEnforcer.enforcePermissionAllOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source);
+ }
+ static final int TRANSACTION_MultiplePermissionsAny = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
+ /** Helper method to enforce permissions for MultiplePermissionsAny */
+ protected void MultiplePermissionsAny_enforcePermission() throws SecurityException {
+ android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
+ mEnforcer.enforcePermissionAnyOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source);
+ }
+ static final int TRANSACTION_NonManifestPermission = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
+ /** Helper method to enforce permissions for NonManifestPermission */
+ protected void NonManifestPermission_enforcePermission() throws SecurityException {
+ android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
+ mEnforcer.enforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, source);
+ }
+ static final int TRANSACTION_SetGranted = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
+ /** @hide */
+ public int getMaxTransactionId()
+ {
+ return 4;
+ }
+ }
+
+ @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+ public void PermissionProtected() throws android.os.RemoteException;
+ @android.annotation.EnforcePermission(allOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE})
+ public void MultiplePermissionsAll() throws android.os.RemoteException;
+ @android.annotation.EnforcePermission(anyOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE})
+ public void MultiplePermissionsAny() throws android.os.RemoteException;
+ @android.annotation.EnforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+ public void NonManifestPermission() throws android.os.RemoteException;
+ // Used by the integration tests to dynamically set permissions that are considered granted.
+ @android.annotation.RequiresNoPermission
+ public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException;
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun test_generated_IProtectedInterface() {
+ lint().files(
+ java(
+ """
+ /*
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+ package android.aidl.tests.permission;
+ public interface IProtectedInterface extends android.os.IInterface
+ {
+ /** Default implementation for IProtectedInterface. */
+ public static class Default implements android.aidl.tests.permission.IProtectedInterface
+ {
+ @Override public void Method1() throws android.os.RemoteException
+ {
+ }
+ @Override public void Method2() throws android.os.RemoteException
+ {
+ }
+ @Override
+ public android.os.IBinder asBinder() {
+ return null;
+ }
+ }
+ /** Local-side IPC implementation stub class. */
+ public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtectedInterface
+ {
+ private final android.os.PermissionEnforcer mEnforcer;
+ /** Construct the stub using the Enforcer provided. */
+ public Stub(android.os.PermissionEnforcer enforcer)
+ {
+ this.attachInterface(this, DESCRIPTOR);
+ if (enforcer == null) {
+ throw new IllegalArgumentException("enforcer cannot be null");
+ }
+ mEnforcer = enforcer;
+ }
+ @Deprecated
+ /** Default constructor. */
+ public Stub() {
+ this(android.os.PermissionEnforcer.fromContext(
+ android.app.ActivityThread.currentActivityThread().getSystemContext()));
+ }
+ /**
+ * Cast an IBinder object into an android.aidl.tests.permission.IProtectedInterface interface,
+ * generating a proxy if needed.
+ */
+ public static android.aidl.tests.permission.IProtectedInterface asInterface(android.os.IBinder obj)
+ {
+ if ((obj==null)) {
+ return null;
+ }
+ android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+ if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtectedInterface))) {
+ return ((android.aidl.tests.permission.IProtectedInterface)iin);
+ }
+ return new android.aidl.tests.permission.IProtectedInterface.Stub.Proxy(obj);
+ }
+ @Override public android.os.IBinder asBinder()
+ {
+ return this;
+ }
+ /** @hide */
+ public static java.lang.String getDefaultTransactionName(int transactionCode)
+ {
+ switch (transactionCode)
+ {
+ case TRANSACTION_Method1:
+ {
+ return "Method1";
+ }
+ case TRANSACTION_Method2:
+ {
+ return "Method2";
+ }
+ default:
+ {
+ return null;
+ }
+ }
+ }
+ /** @hide */
+ public java.lang.String getTransactionName(int transactionCode)
+ {
+ return this.getDefaultTransactionName(transactionCode);
+ }
+ @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
+ {
+ java.lang.String descriptor = DESCRIPTOR;
+ if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
+ data.enforceInterface(descriptor);
+ }
+ switch (code)
+ {
+ case INTERFACE_TRANSACTION:
+ {
+ reply.writeString(descriptor);
+ return true;
+ }
+ }
+ switch (code)
+ {
+ case TRANSACTION_Method1:
+ {
+ this.Method1();
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_Method2:
+ {
+ this.Method2();
+ reply.writeNoException();
+ break;
+ }
+ default:
+ {
+ return super.onTransact(code, data, reply, flags);
+ }
+ }
+ return true;
+ }
+ private static class Proxy implements android.aidl.tests.permission.IProtectedInterface
+ {
+ private android.os.IBinder mRemote;
+ Proxy(android.os.IBinder remote)
+ {
+ mRemote = remote;
+ }
+ @Override public android.os.IBinder asBinder()
+ {
+ return mRemote;
+ }
+ public java.lang.String getInterfaceDescriptor()
+ {
+ return DESCRIPTOR;
+ }
+ @Override public void Method1() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_Method1, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void Method2() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_Method2, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ }
+ static final int TRANSACTION_Method1 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+ /** Helper method to enforce permissions for Method1 */
+ protected void Method1_enforcePermission() throws SecurityException {
+ android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
+ mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source);
+ }
+ static final int TRANSACTION_Method2 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+ /** Helper method to enforce permissions for Method2 */
+ protected void Method2_enforcePermission() throws SecurityException {
+ android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
+ mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source);
+ }
+ /** @hide */
+ public int getMaxTransactionId()
+ {
+ return 1;
+ }
+ }
+
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+ public void Method1() throws android.os.RemoteException;
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+ public void Method2() throws android.os.RemoteException;
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ /* Stubs */
+
+ private val manifestPermissionStub: TestFile = java(
+ """
+ package android.Manifest;
+ class permission {
+ public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
+ public static final String INTERNET = "android.permission.INTERNET";
+ }
+ """
+ ).indented()
+
+ private val enforcePermissionAnnotationStub: TestFile = java(
+ """
+ package android.annotation;
+ public @interface EnforcePermission {}
+ """
+ ).indented()
+
+ private val stubs = arrayOf(manifestPermissionStub, enforcePermissionAnnotationStub)
+}
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt
new file mode 100644
index 000000000000..75b00737a168
--- /dev/null
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt
@@ -0,0 +1,425 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.aidl
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class EnforcePermissionDetectorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = EnforcePermissionDetector()
+
+ override fun getIssues(): List<Issue> = listOf(
+ EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION,
+ EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION
+ )
+
+ override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+ fun testDoesNotDetectIssuesCorrectAnnotationOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ import android.annotation.EnforcePermission;
+ public class TestClass2 extends IFooMethod.Stub {
+ @Override
+ @EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+ public void testMethod() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testDoesNotDetectIssuesCorrectAnnotationAllOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ import android.annotation.EnforcePermission;
+ public class TestClass11 extends IFooMethod.Stub {
+ @Override
+ @EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAll() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testDoesNotDetectIssuesCorrectAnnotationAllLiteralOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ import android.annotation.EnforcePermission;
+ public class TestClass111 extends IFooMethod.Stub {
+ @Override
+ @EnforcePermission(allOf={"android.permission.INTERNET", android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAllLiteral() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testDoesNotDetectIssuesCorrectAnnotationAnyOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ import android.annotation.EnforcePermission;
+ public class TestClass12 extends IFooMethod.Stub {
+ @Override
+ @EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAny() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testDoesNotDetectIssuesCorrectAnnotationAnyLiteralOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ import android.annotation.EnforcePermission;
+ public class TestClass121 extends IFooMethod.Stub {
+ @Override
+ @EnforcePermission(anyOf={"android.permission.INTERNET", android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAnyLiteral() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testDetectIssuesMismatchingAnnotationOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ public class TestClass4 extends IFooMethod.Stub {
+ @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET)
+ public void testMethod() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expect("""
+ src/test/pkg/TestClass4.java:4: Error: The method TestClass4.testMethod is annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \
+ which differs from the overridden method Stub.testMethod: @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). \
+ The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+ public void testMethod() {}
+ ~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
+ }
+
+ fun testDetectIssuesEmptyAnnotationOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ public class TestClass41 extends IFooMethod.Stub {
+ @android.annotation.EnforcePermission
+ public void testMethod() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expect("""
+ src/test/pkg/TestClass41.java:4: Error: The method TestClass41.testMethod is annotated with @android.annotation.EnforcePermission \
+ which differs from the overridden method Stub.testMethod: @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). \
+ The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+ public void testMethod() {}
+ ~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
+ }
+
+ fun testDetectIssuesMismatchingAnyAnnotationOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ public class TestClass9 extends IFooMethod.Stub {
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC})
+ public void testMethodAny() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expect("""
+ src/test/pkg/TestClass9.java:4: Error: The method TestClass9.testMethodAny is annotated with \
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) \
+ which differs from the overridden method Stub.testMethodAny: \
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}). \
+ The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+ public void testMethodAny() {}
+ ~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
+ }
+
+ fun testDetectIssuesMismatchingAnyLiteralAnnotationOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ public class TestClass91 extends IFooMethod.Stub {
+ @android.annotation.EnforcePermission(anyOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"})
+ public void testMethodAnyLiteral() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expect("""
+ src/test/pkg/TestClass91.java:4: Error: The method TestClass91.testMethodAnyLiteral is annotated with \
+ @android.annotation.EnforcePermission(anyOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) \
+ which differs from the overridden method Stub.testMethodAnyLiteral: \
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \
+ The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+ public void testMethodAnyLiteral() {}
+ ~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
+ }
+
+ fun testDetectIssuesMismatchingAllAnnotationOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ public class TestClass10 extends IFooMethod.Stub {
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC})
+ public void testMethodAll() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expect("""
+ src/test/pkg/TestClass10.java:4: Error: The method TestClass10.testMethodAll is annotated with \
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) \
+ which differs from the overridden method Stub.testMethodAll: \
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}). \
+ The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+ public void testMethodAll() {}
+ ~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
+ }
+
+ fun testDetectIssuesMismatchingAllLiteralAnnotationOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ public class TestClass101 extends IFooMethod.Stub {
+ @android.annotation.EnforcePermission(allOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"})
+ public void testMethodAllLiteral() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expect("""
+ src/test/pkg/TestClass101.java:4: Error: The method TestClass101.testMethodAllLiteral is annotated with \
+ @android.annotation.EnforcePermission(allOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) \
+ which differs from the overridden method Stub.testMethodAllLiteral: \
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \
+ The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+ public void testMethodAllLiteral() {}
+ ~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
+ }
+
+ fun testDetectIssuesMissingAnnotationOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ public class TestClass6 extends IFooMethod.Stub {
+ public void testMethod() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expect("""
+ src/test/pkg/TestClass6.java:3: Error: The method TestClass6.testMethod overrides the method Stub.testMethod which is annotated with @EnforcePermission. \
+ The same annotation must be used on TestClass6.testMethod [MissingEnforcePermissionAnnotation]
+ public void testMethod() {}
+ ~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
+ }
+
+ fun testDetectIssuesExtraAnnotationMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ public class TestClass7 extends IBar.Stub {
+ @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET)
+ public void testMethod() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expect("""
+ src/test/pkg/TestClass7.java:4: Error: The method TestClass7.testMethod overrides the method Stub.testMethod which is not annotated with @EnforcePermission. \
+ The same annotation must be used on Stub.testMethod. Did you forget to annotate the AIDL definition? [MissingEnforcePermissionAnnotation]
+ public void testMethod() {}
+ ~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
+ }
+
+ fun testDetectIssuesMissingAnnotationOnMethodWhenClassIsCalledDefault() {
+ lint().files(java(
+ """
+ package test.pkg;
+ public class Default extends IFooMethod.Stub {
+ public void testMethod() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/Default.java:3: Error: The method Default.testMethod \
+ overrides the method Stub.testMethod which is annotated with @EnforcePermission. The same annotation must be used on Default.testMethod [MissingEnforcePermissionAnnotation]
+ public void testMethod() {}
+ ~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testDoesDetectIssuesShortStringsNotAllowed() {
+ lint().files(java(
+ """
+ package test.pkg;
+ import android.annotation.EnforcePermission;
+ public class TestClass121 extends IFooMethod.Stub {
+ @Override
+ @EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"})
+ public void testMethodAnyLiteral() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass121.java:6: Error: The method \
+ TestClass121.testMethodAnyLiteral is annotated with @EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"}) \
+ which differs from the overridden method Stub.testMethodAnyLiteral: \
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \
+ The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+ public void testMethodAnyLiteral() {}
+ ~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ /* Stubs */
+
+ // A service with permission annotation on the method.
+ private val interfaceIFooMethodStub: TestFile = java(
+ """
+ public interface IFooMethod extends android.os.IInterface {
+ public static abstract class Stub extends android.os.Binder implements IFooMethod {
+ @Override
+ @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+ public void testMethod() {}
+ @Override
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAny() {}
+ @Override
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
+ public void testMethodAnyLiteral() {}
+ @Override
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAll() {}
+ @Override
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
+ public void testMethodAllLiteral() {}
+ }
+ @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+ public void testMethod();
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAny() {}
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
+ public void testMethodAnyLiteral() {}
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAll() {}
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
+ public void testMethodAllLiteral() {}
+ }
+ """
+ ).indented()
+
+ // A service without any permission annotation.
+ private val interfaceIBarStub: TestFile = java(
+ """
+ public interface IBar extends android.os.IInterface {
+ public static abstract class Stub extends android.os.Binder implements IBar {
+ @Override
+ public void testMethod() {}
+ }
+ public void testMethod();
+ }
+ """
+ ).indented()
+
+ private val manifestPermissionStub: TestFile = java(
+ """
+ package android.Manifest;
+ class permission {
+ public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
+ public static final String NFC = "android.permission.NFC";
+ public static final String INTERNET = "android.permission.INTERNET";
+ }
+ """
+ ).indented()
+
+ private val enforcePermissionAnnotationStub: TestFile = java(
+ """
+ package android.annotation;
+ public @interface EnforcePermission {}
+ """
+ ).indented()
+
+ private val stubs = arrayOf(interfaceIFooMethodStub, interfaceIBarStub,
+ manifestPermissionStub, enforcePermissionAnnotationStub)
+
+ // Substitutes "backslash + new line" with an empty string to imitate line continuation
+ private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
+}
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorCodegenTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorCodegenTest.kt
new file mode 100644
index 000000000000..5a63bb4084d2
--- /dev/null
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorCodegenTest.kt
@@ -0,0 +1,557 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.aidl
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.checks.infrastructure.TestMode
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class EnforcePermissionHelperDetectorCodegenTest : LintDetectorTest() {
+ override fun getDetector(): Detector = EnforcePermissionHelperDetector()
+
+ override fun getIssues(): List<Issue> = listOf(
+ EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER
+ )
+
+ override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+ fun test_generated_IProtected() {
+ lint().testModes(TestMode.DEFAULT).files(
+ java(
+ """
+ /*
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+ package android.aidl.tests.permission;
+ public interface IProtected extends android.os.IInterface
+ {
+ /** Default implementation for IProtected. */
+ public static class Default implements android.aidl.tests.permission.IProtected
+ {
+ @Override public void PermissionProtected() throws android.os.RemoteException
+ {
+ }
+ @Override public void MultiplePermissionsAll() throws android.os.RemoteException
+ {
+ }
+ @Override public void MultiplePermissionsAny() throws android.os.RemoteException
+ {
+ }
+ @Override public void NonManifestPermission() throws android.os.RemoteException
+ {
+ }
+ // Used by the integration tests to dynamically set permissions that are considered granted.
+ @Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException
+ {
+ }
+ @Override
+ public android.os.IBinder asBinder() {
+ return null;
+ }
+ }
+ /** Local-side IPC implementation stub class. */
+ public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtected
+ {
+ private final android.os.PermissionEnforcer mEnforcer;
+ /** Construct the stub using the Enforcer provided. */
+ public Stub(android.os.PermissionEnforcer enforcer)
+ {
+ this.attachInterface(this, DESCRIPTOR);
+ if (enforcer == null) {
+ throw new IllegalArgumentException("enforcer cannot be null");
+ }
+ mEnforcer = enforcer;
+ }
+ @Deprecated
+ /** Default constructor. */
+ public Stub() {
+ this(android.os.PermissionEnforcer.fromContext(
+ android.app.ActivityThread.currentActivityThread().getSystemContext()));
+ }
+ /**
+ * Cast an IBinder object into an android.aidl.tests.permission.IProtected interface,
+ * generating a proxy if needed.
+ */
+ public static android.aidl.tests.permission.IProtected asInterface(android.os.IBinder obj)
+ {
+ if ((obj==null)) {
+ return null;
+ }
+ android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+ if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtected))) {
+ return ((android.aidl.tests.permission.IProtected)iin);
+ }
+ return new android.aidl.tests.permission.IProtected.Stub.Proxy(obj);
+ }
+ @Override public android.os.IBinder asBinder()
+ {
+ return this;
+ }
+ /** @hide */
+ public static java.lang.String getDefaultTransactionName(int transactionCode)
+ {
+ switch (transactionCode)
+ {
+ case TRANSACTION_PermissionProtected:
+ {
+ return "PermissionProtected";
+ }
+ case TRANSACTION_MultiplePermissionsAll:
+ {
+ return "MultiplePermissionsAll";
+ }
+ case TRANSACTION_MultiplePermissionsAny:
+ {
+ return "MultiplePermissionsAny";
+ }
+ case TRANSACTION_NonManifestPermission:
+ {
+ return "NonManifestPermission";
+ }
+ case TRANSACTION_SetGranted:
+ {
+ return "SetGranted";
+ }
+ default:
+ {
+ return null;
+ }
+ }
+ }
+ /** @hide */
+ public java.lang.String getTransactionName(int transactionCode)
+ {
+ return this.getDefaultTransactionName(transactionCode);
+ }
+ @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
+ {
+ java.lang.String descriptor = DESCRIPTOR;
+ if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
+ data.enforceInterface(descriptor);
+ }
+ switch (code)
+ {
+ case INTERFACE_TRANSACTION:
+ {
+ reply.writeString(descriptor);
+ return true;
+ }
+ }
+ switch (code)
+ {
+ case TRANSACTION_PermissionProtected:
+ {
+ this.PermissionProtected();
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_MultiplePermissionsAll:
+ {
+ this.MultiplePermissionsAll();
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_MultiplePermissionsAny:
+ {
+ this.MultiplePermissionsAny();
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_NonManifestPermission:
+ {
+ this.NonManifestPermission();
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_SetGranted:
+ {
+ java.util.List<java.lang.String> _arg0;
+ _arg0 = data.createStringArrayList();
+ data.enforceNoDataAvail();
+ this.SetGranted(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ default:
+ {
+ return super.onTransact(code, data, reply, flags);
+ }
+ }
+ return true;
+ }
+ private static class Proxy implements android.aidl.tests.permission.IProtected
+ {
+ private android.os.IBinder mRemote;
+ Proxy(android.os.IBinder remote)
+ {
+ mRemote = remote;
+ }
+ @Override public android.os.IBinder asBinder()
+ {
+ return mRemote;
+ }
+ public java.lang.String getInterfaceDescriptor()
+ {
+ return DESCRIPTOR;
+ }
+ @Override public void PermissionProtected() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_PermissionProtected, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void MultiplePermissionsAll() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAll, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void MultiplePermissionsAny() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAny, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void NonManifestPermission() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_NonManifestPermission, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ // Used by the integration tests to dynamically set permissions that are considered granted.
+ @Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeStringList(permissions);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_SetGranted, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ }
+ static final int TRANSACTION_PermissionProtected = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+ /** Helper method to enforce permissions for PermissionProtected */
+ protected void PermissionProtected_enforcePermission() throws SecurityException {
+ android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
+ mEnforcer.enforcePermission(android.Manifest.permission.READ_PHONE_STATE, source);
+ }
+ static final int TRANSACTION_MultiplePermissionsAll = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+ /** Helper method to enforce permissions for MultiplePermissionsAll */
+ protected void MultiplePermissionsAll_enforcePermission() throws SecurityException {
+ android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
+ mEnforcer.enforcePermissionAllOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source);
+ }
+ static final int TRANSACTION_MultiplePermissionsAny = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
+ /** Helper method to enforce permissions for MultiplePermissionsAny */
+ protected void MultiplePermissionsAny_enforcePermission() throws SecurityException {
+ android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
+ mEnforcer.enforcePermissionAnyOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source);
+ }
+ static final int TRANSACTION_NonManifestPermission = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
+ /** Helper method to enforce permissions for NonManifestPermission */
+ protected void NonManifestPermission_enforcePermission() throws SecurityException {
+ android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
+ mEnforcer.enforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, source);
+ }
+ static final int TRANSACTION_SetGranted = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
+ /** @hide */
+ public int getMaxTransactionId()
+ {
+ return 4;
+ }
+ }
+
+ @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+ public void PermissionProtected() throws android.os.RemoteException;
+ @android.annotation.EnforcePermission(allOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE})
+ public void MultiplePermissionsAll() throws android.os.RemoteException;
+ @android.annotation.EnforcePermission(anyOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE})
+ public void MultiplePermissionsAny() throws android.os.RemoteException;
+ @android.annotation.EnforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+ public void NonManifestPermission() throws android.os.RemoteException;
+ // Used by the integration tests to dynamically set permissions that are considered granted.
+ @android.annotation.RequiresNoPermission
+ public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException;
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun test_generated_IProtectedInterface() {
+ lint().files(
+ java(
+ """
+ /*
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+ package android.aidl.tests.permission;
+ public interface IProtectedInterface extends android.os.IInterface
+ {
+ /** Default implementation for IProtectedInterface. */
+ public static class Default implements android.aidl.tests.permission.IProtectedInterface
+ {
+ @Override public void Method1() throws android.os.RemoteException
+ {
+ }
+ @Override public void Method2() throws android.os.RemoteException
+ {
+ }
+ @Override
+ public android.os.IBinder asBinder() {
+ return null;
+ }
+ }
+ /** Local-side IPC implementation stub class. */
+ public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtectedInterface
+ {
+ private final android.os.PermissionEnforcer mEnforcer;
+ /** Construct the stub using the Enforcer provided. */
+ public Stub(android.os.PermissionEnforcer enforcer)
+ {
+ this.attachInterface(this, DESCRIPTOR);
+ if (enforcer == null) {
+ throw new IllegalArgumentException("enforcer cannot be null");
+ }
+ mEnforcer = enforcer;
+ }
+ @Deprecated
+ /** Default constructor. */
+ public Stub() {
+ this(android.os.PermissionEnforcer.fromContext(
+ android.app.ActivityThread.currentActivityThread().getSystemContext()));
+ }
+ /**
+ * Cast an IBinder object into an android.aidl.tests.permission.IProtectedInterface interface,
+ * generating a proxy if needed.
+ */
+ public static android.aidl.tests.permission.IProtectedInterface asInterface(android.os.IBinder obj)
+ {
+ if ((obj==null)) {
+ return null;
+ }
+ android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+ if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtectedInterface))) {
+ return ((android.aidl.tests.permission.IProtectedInterface)iin);
+ }
+ return new android.aidl.tests.permission.IProtectedInterface.Stub.Proxy(obj);
+ }
+ @Override public android.os.IBinder asBinder()
+ {
+ return this;
+ }
+ /** @hide */
+ public static java.lang.String getDefaultTransactionName(int transactionCode)
+ {
+ switch (transactionCode)
+ {
+ case TRANSACTION_Method1:
+ {
+ return "Method1";
+ }
+ case TRANSACTION_Method2:
+ {
+ return "Method2";
+ }
+ default:
+ {
+ return null;
+ }
+ }
+ }
+ /** @hide */
+ public java.lang.String getTransactionName(int transactionCode)
+ {
+ return this.getDefaultTransactionName(transactionCode);
+ }
+ @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
+ {
+ java.lang.String descriptor = DESCRIPTOR;
+ if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
+ data.enforceInterface(descriptor);
+ }
+ switch (code)
+ {
+ case INTERFACE_TRANSACTION:
+ {
+ reply.writeString(descriptor);
+ return true;
+ }
+ }
+ switch (code)
+ {
+ case TRANSACTION_Method1:
+ {
+ this.Method1();
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_Method2:
+ {
+ this.Method2();
+ reply.writeNoException();
+ break;
+ }
+ default:
+ {
+ return super.onTransact(code, data, reply, flags);
+ }
+ }
+ return true;
+ }
+ private static class Proxy implements android.aidl.tests.permission.IProtectedInterface
+ {
+ private android.os.IBinder mRemote;
+ Proxy(android.os.IBinder remote)
+ {
+ mRemote = remote;
+ }
+ @Override public android.os.IBinder asBinder()
+ {
+ return mRemote;
+ }
+ public java.lang.String getInterfaceDescriptor()
+ {
+ return DESCRIPTOR;
+ }
+ @Override public void Method1() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_Method1, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void Method2() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_Method2, _data, _reply, 0);
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ }
+ static final int TRANSACTION_Method1 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+ /** Helper method to enforce permissions for Method1 */
+ protected void Method1_enforcePermission() throws SecurityException {
+ android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
+ mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source);
+ }
+ static final int TRANSACTION_Method2 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+ /** Helper method to enforce permissions for Method2 */
+ protected void Method2_enforcePermission() throws SecurityException {
+ android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
+ mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source);
+ }
+ /** @hide */
+ public int getMaxTransactionId()
+ {
+ return 1;
+ }
+ }
+
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+ public void Method1() throws android.os.RemoteException;
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+ public void Method2() throws android.os.RemoteException;
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ /* Stubs */
+
+ private val manifestPermissionStub: TestFile = java(
+ """
+ package android.Manifest;
+ class permission {
+ public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
+ public static final String INTERNET = "android.permission.INTERNET";
+ }
+ """
+ ).indented()
+
+ private val enforcePermissionAnnotationStub: TestFile = java(
+ """
+ package android.annotation;
+ public @interface EnforcePermission {}
+ """
+ ).indented()
+
+ private val stubs = arrayOf(manifestPermissionStub, enforcePermissionAnnotationStub)
+}
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt
new file mode 100644
index 000000000000..10a6e1da91dc
--- /dev/null
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt
@@ -0,0 +1,443 @@
+/*
+* Copyright (C) 2022 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.google.android.lint.aidl
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+
+class EnforcePermissionHelperDetectorTest : LintDetectorTest() {
+ override fun getDetector() = EnforcePermissionHelperDetector()
+ override fun getIssues() = listOf(
+ EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER,
+ EnforcePermissionHelperDetector.ISSUE_MISUSING_ENFORCE_PERMISSION
+ )
+
+ override fun lint(): TestLintTask = super.lint().allowMissingSdk()
+
+ fun testFirstExpressionIsFunctionCall() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ Binder.getCallingUid();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper]
+ @Override
+ ^
+ 1 errors, 0 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Autofix for src/Foo.java line 5: Replace with test_enforcePermission();...:
+ @@ -8 +8
+ + test_enforcePermission();
+ +
+ """
+ )
+ }
+
+ fun testFirstExpressionIsVariableDeclaration() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ String foo = "bar";
+ Binder.getCallingUid();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper]
+ @Override
+ ^
+ 1 errors, 0 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Autofix for src/Foo.java line 5: Replace with test_enforcePermission();...:
+ @@ -8 +8
+ + test_enforcePermission();
+ +
+ """
+ )
+ }
+
+ fun testMethodIsEmpty() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {}
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper]
+ @Override
+ ^
+ 1 errors, 0 warnings
+ """
+ )
+ }
+
+ fun testOkay() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ super.test_enforcePermission();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testHelperWithoutSuperPrefix_Okay() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ test_enforcePermission();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testInterfaceDefaultMethod_notStubAncestor_error() {
+ lint().files(
+ java(
+ """
+ public interface IProtected extends android.os.IInterface {
+ @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+ default void PermissionProtected() throws android.os.RemoteException {
+ String foo = "bar";
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/IProtected.java:2: Error: The class of PermissionProtected does not inherit from an AIDL generated Stub class [MisusingEnforcePermissionAnnotation]
+ @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+ ^
+ 1 errors, 0 warnings
+ """
+ )
+ }
+
+ fun testInheritance_callSuper_okay() {
+ lint().files(
+ java(
+ """
+ package test;
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ super.test_enforcePermission();
+ }
+ }
+ """
+ ).indented(),
+ java(
+ """
+ package test;
+ import test.Foo;
+ public class Bar extends Foo {
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ super.test();
+ }
+ }
+ """
+ ).indented(),
+ java(
+ """
+ package test;
+ import test.Bar;
+ public class Baz extends Bar {
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ super.test();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testInheritance_callHelper_okay() {
+ lint().files(
+ java(
+ """
+ package test;
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ super.test_enforcePermission();
+ }
+ }
+ """
+ ).indented(),
+ java(
+ """
+ package test;
+ import test.Foo;
+ public class Bar extends Foo {
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ super.test();
+ }
+ }
+ """
+ ).indented(),
+ java(
+ """
+ package test;
+ import test.Bar;
+ public class Baz extends Bar {
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ super.test_enforcePermission();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testInheritance_missingCallInChain_error() {
+ lint().files(
+ java(
+ """
+ package test;
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ super.test_enforcePermission();
+ }
+ }
+ """
+ ).indented(),
+ java(
+ """
+ package test;
+ import test.Foo;
+ public class Bar extends Foo {
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ doStuff();
+ }
+ }
+ """
+ ).indented(),
+ java(
+ """
+ package test;
+ import test.Bar;
+ public class Baz extends Bar {
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ super.test();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/test/Bar.java:4: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper]
+ @Override
+ ^
+ 1 errors, 0 warnings
+ """
+ )
+ }
+
+ fun testInheritance_missingCall_error() {
+ lint().files(
+ java(
+ """
+ package test;
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ super.test_enforcePermission();
+ }
+ }
+ """
+ ).indented(),
+ java(
+ """
+ package test;
+ import test.Foo;
+ public class Bar extends Foo {
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ super.test();
+ }
+ }
+ """
+ ).indented(),
+ java(
+ """
+ package test;
+ import test.Bar;
+ public class Baz extends Bar {
+ @Override
+ @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+ public void test() throws android.os.RemoteException {
+ doStuff();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/test/Baz.java:4: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper]
+ @Override
+ ^
+ 1 errors, 0 warnings
+ """
+ )
+ }
+
+ fun testRandomClass_notStubAncestor_error() {
+ lint().files(
+ java(
+ """
+ public class Foo {
+ @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+ void PermissionProtected() throws android.os.RemoteException {
+ String foo = "bar";
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:2: Error: The class of PermissionProtected does not inherit from an AIDL generated Stub class [MisusingEnforcePermissionAnnotation]
+ @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+ ^
+ 1 errors, 0 warnings
+ """
+ )
+ }
+
+ companion object {
+ val stubs = arrayOf(aidlStub, contextStub, binderStub)
+ }
+}
+
+
+
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt
new file mode 100644
index 000000000000..6b8e72cf9222
--- /dev/null
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt
@@ -0,0 +1,843 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.aidl
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = SimpleManualPermissionEnforcementDetector()
+ override fun getIssues(): List<Issue> = listOf(
+ SimpleManualPermissionEnforcementDetector
+ .ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT
+ )
+
+ override fun lint(): TestLintTask = super.lint().allowMissingSdk()
+
+ fun testClass() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ public void test() throws android.os.RemoteException {
+ mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:7: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
+ @@ -5 +5
+ + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+ @@ -7 +8
+ - mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+ + test_enforcePermission();
+ """
+ )
+ }
+
+ fun testClass_orSelfFalse_warning() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ public void test() throws android.os.RemoteException {
+ mContext.enforceCallingPermission("android.permission.READ_CONTACTS", "foo");
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ mContext.enforceCallingPermission("android.permission.READ_CONTACTS", "foo");
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
+ @@ -5 +5
+ + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+ @@ -7 +8
+ - mContext.enforceCallingPermission("android.permission.READ_CONTACTS", "foo");
+ + test_enforcePermission();
+ """
+ )
+ }
+
+ fun testClass_enforcesFalse_warning() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ public void test() throws android.os.RemoteException {
+ mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
+ @@ -5 +5
+ + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+ @@ -7 +8
+ - mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+ + test_enforcePermission();
+ """
+ )
+ }
+
+ fun testAnonClass() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo {
+ private Context mContext;
+ private ITest itest = new ITest.Stub() {
+ @Override
+ public void test() throws android.os.RemoteException {
+ mContext.enforceCallingOrSelfPermission(
+ "android.permission.READ_CONTACTS", "foo");
+ }
+ };
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:8: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ mContext.enforceCallingOrSelfPermission(
+ ^
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 8: Annotate with @EnforcePermission:
+ @@ -6 +6
+ + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+ @@ -8 +9
+ - mContext.enforceCallingOrSelfPermission(
+ - "android.permission.READ_CONTACTS", "foo");
+ + test_enforcePermission();
+ """
+ )
+ }
+
+ fun testConstantEvaluation() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ public void test() throws android.os.RemoteException {
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
+ }
+ }
+ """
+ ).indented(),
+ *stubs,
+ manifestStub
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:8: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 8: Annotate with @EnforcePermission:
+ @@ -6 +6
+ + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+ @@ -8 +9
+ - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
+ + test_enforcePermission();
+ """
+ )
+ }
+
+ fun testAllOf() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo {
+ private Context mContext;
+ private ITest itest = new ITest.Stub() {
+ @Override
+ public void test() throws android.os.RemoteException {
+ mContext.enforceCallingOrSelfPermission(
+ "android.permission.READ_CONTACTS", "foo");
+ mContext.enforceCallingOrSelfPermission(
+ "android.permission.WRITE_CONTACTS", "foo");
+ }
+ };
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:10: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ mContext.enforceCallingOrSelfPermission(
+ ^
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 10: Annotate with @EnforcePermission:
+ @@ -6 +6
+ + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"})
+ @@ -8 +9
+ - mContext.enforceCallingOrSelfPermission(
+ - "android.permission.READ_CONTACTS", "foo");
+ - mContext.enforceCallingOrSelfPermission(
+ - "android.permission.WRITE_CONTACTS", "foo");
+ + test_enforcePermission();
+ """
+ )
+ }
+
+ fun testAllOf_mixedOrSelf_warning() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo {
+ private Context mContext;
+ private ITest itest = new ITest.Stub() {
+ @Override
+ public void test() throws android.os.RemoteException {
+ mContext.enforceCallingOrSelfPermission(
+ "android.permission.READ_CONTACTS", "foo");
+ mContext.enforceCallingPermission(
+ "android.permission.WRITE_CONTACTS", "foo");
+ }
+ };
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:10: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ mContext.enforceCallingPermission(
+ ^
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 10: Annotate with @EnforcePermission:
+ @@ -6 +6
+ + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"})
+ @@ -8 +9
+ - mContext.enforceCallingOrSelfPermission(
+ - "android.permission.READ_CONTACTS", "foo");
+ - mContext.enforceCallingPermission(
+ - "android.permission.WRITE_CONTACTS", "foo");
+ + test_enforcePermission();
+ """
+ )
+ }
+
+ fun testAllOf_mixedEnforces_warning() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo {
+ private Context mContext;
+ private ITest itest = new ITest.Stub() {
+ @Override
+ public void test() throws android.os.RemoteException {
+ mContext.enforceCallingOrSelfPermission(
+ "android.permission.READ_CONTACTS", "foo");
+ mContext.checkCallingOrSelfPermission(
+ "android.permission.WRITE_CONTACTS", "foo");
+ }
+ };
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:10: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ mContext.checkCallingOrSelfPermission(
+ ^
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 10: Annotate with @EnforcePermission:
+ @@ -6 +6
+ + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"})
+ @@ -8 +9
+ - mContext.enforceCallingOrSelfPermission(
+ - "android.permission.READ_CONTACTS", "foo");
+ - mContext.checkCallingOrSelfPermission(
+ - "android.permission.WRITE_CONTACTS", "foo");
+ + test_enforcePermission();
+ """
+ )
+ }
+
+ fun testPrecedingExpressions() {
+ lint().files(
+ java(
+ """
+ import android.os.Binder;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private mContext Context;
+ @Override
+ public void test() throws android.os.RemoteException {
+ long uid = Binder.getCallingUid();
+ mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testPermissionHelper() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+
+ @android.annotation.PermissionMethod(orSelf = true)
+ private void helper() {
+ mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+ }
+
+ @Override
+ public void test() throws android.os.RemoteException {
+ helper();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:14: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ helper();
+ ~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 14: Annotate with @EnforcePermission:
+ @@ -12 +12
+ + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+ @@ -14 +15
+ - helper();
+ + test_enforcePermission();
+ """
+ )
+ }
+
+ fun testPermissionHelper_orSelfNotBubbledUp_warning() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+
+ @android.annotation.PermissionMethod
+ private void helper() {
+ mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+ }
+
+ @Override
+ public void test() throws android.os.RemoteException {
+ helper();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:14: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ helper();
+ ~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 14: Annotate with @EnforcePermission:
+ @@ -12 +12
+ + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+ @@ -14 +15
+ - helper();
+ + test_enforcePermission();
+ """
+ )
+ }
+
+ fun testPermissionHelperAllOf() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+
+ @android.annotation.PermissionMethod(orSelf = true)
+ private void helper() {
+ mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+ mContext.enforceCallingOrSelfPermission("android.permission.WRITE_CONTACTS", "foo");
+ }
+
+ @Override
+ public void test() throws android.os.RemoteException {
+ helper();
+ mContext.enforceCallingOrSelfPermission("FOO", "foo");
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:16: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ mContext.enforceCallingOrSelfPermission("FOO", "foo");
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 16: Annotate with @EnforcePermission:
+ @@ -13 +13
+ + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS", "FOO"})
+ @@ -15 +16
+ - helper();
+ - mContext.enforceCallingOrSelfPermission("FOO", "foo");
+ + test_enforcePermission();
+ """
+ )
+ }
+
+
+ fun testPermissionHelperNested() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+
+ @android.annotation.PermissionMethod(orSelf = true)
+ private void helperHelper() {
+ helper("android.permission.WRITE_CONTACTS");
+ }
+
+ @android.annotation.PermissionMethod(orSelf = true)
+ private void helper(@android.annotation.PermissionName String extraPermission) {
+ mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+ }
+
+ @Override
+ public void test() throws android.os.RemoteException {
+ helperHelper();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:19: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ helperHelper();
+ ~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 19: Annotate with @EnforcePermission:
+ @@ -17 +17
+ + @android.annotation.EnforcePermission(allOf={"android.permission.WRITE_CONTACTS", "android.permission.READ_CONTACTS"})
+ @@ -19 +20
+ - helperHelper();
+ + test_enforcePermission();
+ """
+ )
+ }
+
+ fun testIfExpression() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ public void test() throws android.os.RemoteException {
+ if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo")
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("yikes!");
+ }
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:7: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo")
+ ^
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
+ @@ -5 +5
+ + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+ @@ -7 +8
+ - if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo")
+ - != PackageManager.PERMISSION_GRANTED) {
+ - throw new SecurityException("yikes!");
+ - }
+ + test_enforcePermission();
+ """
+ )
+ }
+
+ fun testIfExpression_orSelfFalse_warning() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ public void test() throws android.os.RemoteException {
+ if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo")
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("yikes!");
+ }
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo")
+ ^
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
+ @@ -5 +5
+ + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+ @@ -7 +8
+ - if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo")
+ - != PackageManager.PERMISSION_GRANTED) {
+ - throw new SecurityException("yikes!");
+ - }
+ + test_enforcePermission();
+ """
+ )
+ }
+
+ fun testIfExpression_otherSideEffect_ignored() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ public void test() throws android.os.RemoteException {
+ if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo")
+ != PackageManager.PERMISSION_GRANTED) {
+ doSomethingElse();
+ throw new SecurityException("yikes!");
+ }
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testIfExpression_inlinedWithSideEffect_ignored() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ public void test() throws android.os.RemoteException {
+ if (somethingElse() && mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo")
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("yikes!");
+ }
+ }
+
+ private boolean somethingElse() {
+ return true;
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testAnyOf_hardCodedAndVarArgs() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+
+ @android.annotation.PermissionMethod(anyOf = true)
+ private void helperHelper() {
+ helper("FOO", "BAR");
+ }
+
+ @android.annotation.PermissionMethod(anyOf = true, value = {"BAZ", "BUZZ"})
+ private void helper(@android.annotation.PermissionName String... extraPermissions) {}
+
+ @Override
+ public void test() throws android.os.RemoteException {
+ helperHelper();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:17: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ helperHelper();
+ ~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 17: Annotate with @EnforcePermission:
+ @@ -15 +15
+ + @android.annotation.EnforcePermission(anyOf={"BAZ", "BUZZ", "FOO", "BAR"})
+ @@ -17 +18
+ - helperHelper();
+ + test_enforcePermission();
+ """
+ )
+ }
+
+
+ fun testAnyOfAllOf_mixedConsecutiveCalls_ignored() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+
+ @android.annotation.PermissionMethod
+ private void allOfhelper() {
+ mContext.enforceCallingOrSelfPermission("FOO");
+ mContext.enforceCallingOrSelfPermission("BAR");
+ }
+
+ @android.annotation.PermissionMethod(anyOf = true, permissions = {"BAZ", "BUZZ"})
+ private void anyOfHelper() {}
+
+ @Override
+ public void test() throws android.os.RemoteException {
+ allOfhelper();
+ anyOfHelper();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testAnyOfAllOf_mixedNestedCalls_ignored() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.annotation.PermissionName;
+ import android.test.ITest;
+
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+
+ @android.annotation.PermissionMethod(anyOf = true)
+ private void anyOfCheck(@PermissionName String... permissions) {
+ allOfCheck("BAZ", "BUZZ");
+ }
+
+ @android.annotation.PermissionMethod
+ private void allOfCheck(@PermissionName String... permissions) {}
+
+ @Override
+ public void test() throws android.os.RemoteException {
+ anyOfCheck("FOO", "BAR");
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ companion object {
+ val stubs = arrayOf(
+ aidlStub,
+ contextStub,
+ binderStub,
+ permissionMethodStub,
+ permissionNameStub
+ )
+ }
+}
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt
new file mode 100644
index 000000000000..2ec8fddbb4e9
--- /dev/null
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt
@@ -0,0 +1,88 @@
+package com.google.android.lint.aidl
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
+import com.android.tools.lint.checks.infrastructure.TestFile
+
+val aidlStub: TestFile = java(
+ """
+ package android.test;
+ public interface ITest extends android.os.IInterface {
+ public static abstract class Stub extends android.os.Binder implements android.test.ITest {
+ protected void test_enforcePermission() throws SecurityException {}
+ }
+ public void test() throws android.os.RemoteException;
+ }
+ """
+).indented()
+
+val contextStub: TestFile = java(
+ """
+ package android.content;
+ public class Context {
+ @android.annotation.PermissionMethod(orSelf = true)
+ public void enforceCallingOrSelfPermission(@android.annotation.PermissionName String permission, String message) {}
+ @android.annotation.PermissionMethod
+ public void enforceCallingPermission(@android.annotation.PermissionName String permission, String message) {}
+ @android.annotation.PermissionMethod(orSelf = true)
+ public int checkCallingOrSelfPermission(@android.annotation.PermissionName String permission, String message) {}
+ @android.annotation.PermissionMethod
+ public int checkCallingPermission(@android.annotation.PermissionName String permission, String message) {}
+ }
+ """
+).indented()
+
+val binderStub: TestFile = java(
+ """
+ package android.os;
+ public class Binder {
+ public static int getCallingUid() {}
+ }
+ """
+).indented()
+
+val permissionMethodStub: TestFile = java(
+ """
+ package android.annotation;
+
+ import static java.lang.annotation.ElementType.METHOD;
+ import static java.lang.annotation.RetentionPolicy.CLASS;
+
+ import java.lang.annotation.Retention;
+ import java.lang.annotation.Target;
+
+ @Retention(CLASS)
+ @Target({METHOD})
+ public @interface PermissionMethod {}
+ """
+).indented()
+
+val permissionNameStub: TestFile = java(
+ """
+ package android.annotation;
+
+ import static java.lang.annotation.ElementType.FIELD;
+ import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+ import static java.lang.annotation.ElementType.METHOD;
+ import static java.lang.annotation.ElementType.PARAMETER;
+ import static java.lang.annotation.RetentionPolicy.CLASS;
+
+ import java.lang.annotation.Retention;
+ import java.lang.annotation.Target;
+
+ @Retention(CLASS)
+ @Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD})
+ public @interface PermissionName {}
+ """
+).indented()
+
+val manifestStub: TestFile = java(
+ """
+ package android;
+
+ public final class Manifest {
+ public static final class permission {
+ public static final String READ_CONTACTS="android.permission.READ_CONTACTS";
+ }
+ }
+ """.trimIndent()
+) \ No newline at end of file
diff --git a/tools/localedata/OWNERS b/tools/localedata/OWNERS
new file mode 100644
index 000000000000..2501679784d6
--- /dev/null
+++ b/tools/localedata/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 24949
+include platform/external/icu:/OWNERS
diff --git a/tools/localedata/extract_icu_data.py b/tools/localedata/extract_icu_data.py
index ca1847af7d06..81ac897deae0 100755
--- a/tools/localedata/extract_icu_data.py
+++ b/tools/localedata/extract_icu_data.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
#
# Copyright 2016 The Android Open Source Project. All Rights Reserved.
#
@@ -61,7 +61,7 @@ def read_likely_subtags(input_file_name):
# would be chosen.)
}
for line in input_file:
- line = unicode(line, 'UTF-8').strip(u' \n\uFEFF').encode('UTF-8')
+ line = line.strip(u' \n\uFEFF')
if line.startswith('//'):
continue
if '{' in line and '}' in line:
@@ -118,26 +118,26 @@ def pack_to_uint32(locale):
def dump_script_codes(all_scripts):
"""Dump the SCRIPT_CODES table."""
- print 'const char SCRIPT_CODES[][4] = {'
+ print('const char SCRIPT_CODES[][4] = {')
for index, script in enumerate(all_scripts):
- print " /* %-2d */ {'%c', '%c', '%c', '%c'}," % (
- index, script[0], script[1], script[2], script[3])
- print '};'
- print
+ print(" /* %-2d */ {'%c', '%c', '%c', '%c'}," % (
+ index, script[0], script[1], script[2], script[3]))
+ print('};')
+ print()
def dump_script_data(likely_script_dict, all_scripts):
"""Dump the script data."""
- print
- print 'const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({'
+ print()
+ print('const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({')
for locale in sorted(likely_script_dict.keys()):
script = likely_script_dict[locale]
- print ' {0x%08Xu, %2du}, // %s -> %s' % (
+ print(' {0x%08Xu, %2du}, // %s -> %s' % (
pack_to_uint32(locale),
all_scripts.index(script),
locale.replace('_', '-'),
- script)
- print '});'
+ script))
+ print('});')
def pack_to_uint64(locale):
@@ -152,13 +152,13 @@ def pack_to_uint64(locale):
def dump_representative_locales(representative_locales):
"""Dump the set of representative locales."""
- print
- print 'std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({'
+ print()
+ print('std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({')
for locale in sorted(representative_locales):
- print ' 0x%08XLLU, // %s' % (
+ print(' 0x%08XLLU, // %s' % (
pack_to_uint64(locale),
- locale)
- print '});'
+ locale))
+ print('});')
def read_and_dump_likely_data(icu_data_dir):
@@ -220,30 +220,30 @@ def get_likely_script(locale, likely_script_dict):
def dump_parent_data(script_organized_dict):
"""Dump information for parents of locales."""
sorted_scripts = sorted(script_organized_dict.keys())
- print
+ print()
for script in sorted_scripts:
parent_dict = script_organized_dict[script]
print ('const std::unordered_map<uint32_t, uint32_t> %s_PARENTS({'
% escape_script_variable_name(script.upper()))
for locale in sorted(parent_dict.keys()):
parent = parent_dict[locale]
- print ' {0x%08Xu, 0x%08Xu}, // %s -> %s' % (
+ print(' {0x%08Xu, 0x%08Xu}, // %s -> %s' % (
pack_to_uint32(locale),
pack_to_uint32(parent),
locale.replace('_', '-'),
- parent.replace('_', '-'))
- print '});'
- print
-
- print 'const struct {'
- print ' const char script[4];'
- print ' const std::unordered_map<uint32_t, uint32_t>* map;'
- print '} SCRIPT_PARENTS[] = {'
+ parent.replace('_', '-')))
+ print('});')
+ print()
+
+ print('const struct {')
+ print(' const char script[4];')
+ print(' const std::unordered_map<uint32_t, uint32_t>* map;')
+ print('} SCRIPT_PARENTS[] = {')
for script in sorted_scripts:
- print " {{'%c', '%c', '%c', '%c'}, &%s_PARENTS}," % (
+ print(" {{'%c', '%c', '%c', '%c'}, &%s_PARENTS}," % (
script[0], script[1], script[2], script[3],
- escape_script_variable_name(script.upper()))
- print '};'
+ escape_script_variable_name(script.upper())))
+ print('};')
def dump_parent_tree_depth(parent_dict):
@@ -256,8 +256,8 @@ def dump_parent_tree_depth(parent_dict):
depth += 1
max_depth = max(max_depth, depth)
assert max_depth < 5 # Our algorithms assume small max_depth
- print
- print 'const size_t MAX_PARENT_DEPTH = %d;' % max_depth
+ print()
+ print('const size_t MAX_PARENT_DEPTH = %d;' % max_depth)
def read_and_dump_parent_data(icu_data_dir, likely_script_dict):
@@ -281,8 +281,8 @@ def main():
source_root,
'external', 'icu', 'icu4c', 'source', 'data')
- print '// Auto-generated by %s' % sys.argv[0]
- print
+ print('// Auto-generated by %s' % sys.argv[0])
+ print()
likely_script_dict = read_and_dump_likely_data(icu_data_dir)
read_and_dump_parent_data(icu_data_dir, likely_script_dict)
diff --git a/tools/locked_region_code_injection/Android.bp b/tools/locked_region_code_injection/Android.bp
index 6efd1f64d8fe..954b816a52bf 100644
--- a/tools/locked_region_code_injection/Android.bp
+++ b/tools/locked_region_code_injection/Android.bp
@@ -12,10 +12,27 @@ java_binary_host {
manifest: "manifest.txt",
srcs: ["src/**/*.java"],
static_libs: [
- "asm-9.2",
- "asm-commons-9.2",
- "asm-tree-9.2",
- "asm-analysis-9.2",
- "guava-21.0",
+ "guava",
+ "ow2-asm",
+ "ow2-asm-analysis",
+ "ow2-asm-commons",
+ "ow2-asm-tree",
+ ],
+}
+
+java_library_host {
+ name: "lockedregioncodeinjection_input",
+ manifest: "test/manifest.txt",
+ srcs: ["test/*/*.java"],
+ static_libs: [
+ "guava",
+ "ow2-asm",
+ "ow2-asm-analysis",
+ "ow2-asm-commons",
+ "ow2-asm-tree",
+ "hamcrest-library",
+ "hamcrest",
+ "platform-test-annotations",
+ "junit",
],
}
diff --git a/tools/locked_region_code_injection/OWNERS b/tools/locked_region_code_injection/OWNERS
new file mode 100644
index 000000000000..bd43f1736ca5
--- /dev/null
+++ b/tools/locked_region_code_injection/OWNERS
@@ -0,0 +1,4 @@
+# Everyone in frameworks/base is included by default
+shayba@google.com
+shombert@google.com
+timmurray@google.com
diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java
index 81a077324e6c..2067bb4ef2fe 100644
--- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java
+++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java
@@ -13,37 +13,51 @@
*/
package lockedregioncodeinjection;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedList;
-import java.util.List;
+import static com.google.common.base.Preconditions.checkElementIndex;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.TryCatchBlockSorter;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnList;
+import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.LabelNode;
+import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
+import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.TryCatchBlockNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicValue;
import org.objectweb.asm.tree.analysis.Frame;
-import static com.google.common.base.Preconditions.checkElementIndex;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
/**
- * This visitor does two things:
+ * This visitor operates on two kinds of targets. For a legacy target, it does the following:
*
- * 1. Finds all the MONITOR_ENTER / MONITOR_EXIT in the byte code and insert the corresponding pre
+ * 1. Finds all the MONITOR_ENTER / MONITOR_EXIT in the byte code and inserts the corresponding pre
* and post methods calls should it matches one of the given target type in the Configuration.
*
* 2. Find all methods that are synchronized and insert pre method calls in the beginning and post
* method calls just before all return instructions.
+ *
+ * For a scoped target, it does the following:
+ *
+ * 1. Finds all the MONITOR_ENTER instructions in the byte code. If the target of the opcode is
+ * named in a --scope switch, then the pre method is invoked ON THE TARGET immediately after
+ * MONITOR_ENTER opcode completes.
+ *
+ * 2. Finds all the MONITOR_EXIT instructions in the byte code. If the target of the opcode is
+ * named in a --scope switch, then the post method is invoked ON THE TARGET immediately before
+ * MONITOR_EXIT opcode completes.
*/
class LockFindingClassVisitor extends ClassVisitor {
private String className = null;
@@ -73,12 +87,16 @@ class LockFindingClassVisitor extends ClassVisitor {
class LockFindingMethodVisitor extends MethodVisitor {
private String owner;
private MethodVisitor chain;
+ private final String className;
+ private final String methodName;
public LockFindingMethodVisitor(String owner, MethodNode mn, MethodVisitor chain) {
super(Utils.ASM_VERSION, mn);
assert owner != null;
this.owner = owner;
this.chain = chain;
+ className = owner;
+ methodName = mn.name;
}
@SuppressWarnings("unchecked")
@@ -93,6 +111,12 @@ class LockFindingClassVisitor extends ClassVisitor {
for (LockTarget t : targets) {
if (t.getTargetDesc().equals("L" + owner + ";")) {
ownerMonitor = t;
+ if (ownerMonitor.getScoped()) {
+ final String emsg = String.format(
+ "scoped targets do not support synchronized methods in %s.%s()",
+ className, methodName);
+ throw new RuntimeException(emsg);
+ }
}
}
}
@@ -118,9 +142,11 @@ class LockFindingClassVisitor extends ClassVisitor {
AbstractInsnNode s = instructions.getFirst();
MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC,
ownerMonitor.getPreOwner(), ownerMonitor.getPreMethod(), "()V", false);
- insertMethodCallBefore(mn, frameMap, handlersMap, s, 0, call);
+ insertMethodCallBeforeSync(mn, frameMap, handlersMap, s, 0, call);
}
+ boolean anyDup = false;
+
for (int i = 0; i < instructions.size(); i++) {
AbstractInsnNode s = instructions.get(i);
@@ -131,9 +157,15 @@ class LockFindingClassVisitor extends ClassVisitor {
LockTargetState state = (LockTargetState) operand;
for (int j = 0; j < state.getTargets().size(); j++) {
LockTarget target = state.getTargets().get(j);
- MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC,
- target.getPreOwner(), target.getPreMethod(), "()V", false);
- insertMethodCallAfter(mn, frameMap, handlersMap, s, i, call);
+ MethodInsnNode call = methodCall(target, true);
+ if (target.getScoped()) {
+ TypeInsnNode cast = typeCast(target);
+ i += insertInvokeAcquire(mn, frameMap, handlersMap, s, i,
+ call, cast);
+ anyDup = true;
+ } else {
+ i += insertMethodCallBefore(mn, frameMap, handlersMap, s, i, call);
+ }
}
}
}
@@ -144,8 +176,9 @@ class LockFindingClassVisitor extends ClassVisitor {
if (operand instanceof LockTargetState) {
LockTargetState state = (LockTargetState) operand;
for (int j = 0; j < state.getTargets().size(); j++) {
- // The instruction after a monitor_exit should be a label for the end of the implicit
- // catch block that surrounds the synchronized block to call monitor_exit when an exception
+ // The instruction after a monitor_exit should be a label for
+ // the end of the implicit catch block that surrounds the
+ // synchronized block to call monitor_exit when an exception
// occurs.
checkState(instructions.get(i + 1).getType() == AbstractInsnNode.LABEL,
"Expected to find label after monitor exit");
@@ -161,9 +194,16 @@ class LockFindingClassVisitor extends ClassVisitor {
"Expected label to be the end of monitor exit's try block");
LockTarget target = state.getTargets().get(j);
- MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC,
- target.getPostOwner(), target.getPostMethod(), "()V", false);
- insertMethodCallAfter(mn, frameMap, handlersMap, label, labelIndex, call);
+ MethodInsnNode call = methodCall(target, false);
+ if (target.getScoped()) {
+ TypeInsnNode cast = typeCast(target);
+ i += insertInvokeRelease(mn, frameMap, handlersMap, s, i,
+ call, cast);
+ anyDup = true;
+ } else {
+ insertMethodCallAfter(mn, frameMap, handlersMap, label,
+ labelIndex, call);
+ }
}
}
}
@@ -174,16 +214,116 @@ class LockFindingClassVisitor extends ClassVisitor {
MethodInsnNode call =
new MethodInsnNode(Opcodes.INVOKESTATIC, ownerMonitor.getPostOwner(),
ownerMonitor.getPostMethod(), "()V", false);
- insertMethodCallBefore(mn, frameMap, handlersMap, s, i, call);
+ insertMethodCallBeforeSync(mn, frameMap, handlersMap, s, i, call);
i++; // Skip ahead. Otherwise, we will revisit this instruction again.
}
}
+
+ if (anyDup) {
+ mn.maxStack++;
+ }
+
super.visitEnd();
mn.accept(chain);
}
+
+ // Insert a call to a monitor pre handler. The node and the index identify the
+ // monitorenter call itself. Insert DUP immediately prior to the MONITORENTER.
+ // Insert the typecast and call (in that order) after the MONITORENTER.
+ public int insertInvokeAcquire(MethodNode mn, List<Frame> frameMap,
+ List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index,
+ MethodInsnNode call, TypeInsnNode cast) {
+ InsnList instructions = mn.instructions;
+
+ // Insert a DUP right before MONITORENTER, to capture the object being locked.
+ // Note that the object will be typed as java.lang.Object.
+ instructions.insertBefore(node, new InsnNode(Opcodes.DUP));
+ frameMap.add(index, frameMap.get(index));
+ handlersMap.add(index, handlersMap.get(index));
+
+ // Insert the call right after the MONITORENTER. These entries are pushed after
+ // MONITORENTER so they are inserted in reverse order. MONITORENTER should be
+ // the target of a try/catch block, which means it must be immediately
+ // followed by a label (which is part of the try/catch block definition).
+ // Move forward past the label so the invocation in inside the proper block.
+ // Throw an error if the next instruction is not a label.
+ node = node.getNext();
+ if (!(node instanceof LabelNode)) {
+ throw new RuntimeException(String.format("invalid bytecode sequence in %s.%s()",
+ className, methodName));
+ }
+ node = node.getNext();
+ index = instructions.indexOf(node);
+
+ instructions.insertBefore(node, cast);
+ frameMap.add(index, frameMap.get(index));
+ handlersMap.add(index, handlersMap.get(index));
+
+ instructions.insertBefore(node, call);
+ frameMap.add(index, frameMap.get(index));
+ handlersMap.add(index, handlersMap.get(index));
+
+ return 3;
+ }
+
+ // Insert instructions completely before the current opcode. This is slightly
+ // different from insertMethodCallBefore(), which inserts the call before MONITOREXIT
+ // but inserts the start and end labels after MONITOREXIT.
+ public int insertInvokeRelease(MethodNode mn, List<Frame> frameMap,
+ List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index,
+ MethodInsnNode call, TypeInsnNode cast) {
+ InsnList instructions = mn.instructions;
+
+ instructions.insertBefore(node, new InsnNode(Opcodes.DUP));
+ frameMap.add(index, frameMap.get(index));
+ handlersMap.add(index, handlersMap.get(index));
+
+ instructions.insertBefore(node, cast);
+ frameMap.add(index, frameMap.get(index));
+ handlersMap.add(index, handlersMap.get(index));
+
+ instructions.insertBefore(node, call);
+ frameMap.add(index, frameMap.get(index));
+ handlersMap.add(index, handlersMap.get(index));
+
+ return 3;
+ }
+ }
+
+ public static MethodInsnNode methodCall(LockTarget target, boolean pre) {
+ String spec = "()V";
+ if (!target.getScoped()) {
+ if (pre) {
+ return new MethodInsnNode(
+ Opcodes.INVOKESTATIC, target.getPreOwner(), target.getPreMethod(), spec);
+ } else {
+ return new MethodInsnNode(
+ Opcodes.INVOKESTATIC, target.getPostOwner(), target.getPostMethod(), spec);
+ }
+ } else {
+ if (pre) {
+ return new MethodInsnNode(
+ Opcodes.INVOKEVIRTUAL, target.getPreOwner(), target.getPreMethod(), spec);
+ } else {
+ return new MethodInsnNode(
+ Opcodes.INVOKEVIRTUAL, target.getPostOwner(), target.getPostMethod(), spec);
+ }
+ }
}
- public static void insertMethodCallBefore(MethodNode mn, List<Frame> frameMap,
+ public static TypeInsnNode typeCast(LockTarget target) {
+ if (!target.getScoped()) {
+ return null;
+ } else {
+ // preOwner and postOwner return the same string for scoped targets.
+ return new TypeInsnNode(Opcodes.CHECKCAST, target.getPreOwner());
+ }
+ }
+
+ /**
+ * Insert a method call before the beginning or end of a synchronized method.
+ */
+ public static void insertMethodCallBeforeSync(MethodNode mn, List<Frame> frameMap,
List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index,
MethodInsnNode call) {
List<TryCatchBlockNode> handlers = handlersMap.get(index);
@@ -226,6 +366,22 @@ class LockFindingClassVisitor extends ClassVisitor {
updateCatchHandler(mn, handlers, start, end, handlersMap);
}
+ // Insert instructions completely before the current opcode. This is slightly different from
+ // insertMethodCallBeforeSync(), which inserts the call before MONITOREXIT but inserts the
+ // start and end labels after MONITOREXIT.
+ public int insertMethodCallBefore(MethodNode mn, List<Frame> frameMap,
+ List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index,
+ MethodInsnNode call) {
+ InsnList instructions = mn.instructions;
+
+ instructions.insertBefore(node, call);
+ frameMap.add(index, frameMap.get(index));
+ handlersMap.add(index, handlersMap.get(index));
+
+ return 1;
+ }
+
+
@SuppressWarnings("unchecked")
public static void updateCatchHandler(MethodNode mn, List<TryCatchBlockNode> handlers,
LabelNode start, LabelNode end, List<List<TryCatchBlockNode>> handlersMap) {
diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java
index c5e59e3dc64d..5f6240327a7f 100644
--- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java
+++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java
@@ -21,14 +21,28 @@ package lockedregioncodeinjection;
public class LockTarget {
public static final LockTarget NO_TARGET = new LockTarget("", null, null);
+ // The lock which must be instrumented, in Java internal form (L<path>;).
private final String targetDesc;
+ // The methods to be called when the lock is taken (released). For non-scoped locks,
+ // these are fully qualified static methods. For scoped locks, these are the
+ // unqualified names of a member method of the target lock.
private final String pre;
private final String post;
+ // If true, the pre and post methods are virtual on the target class. The pre and post methods
+ // are both called while the lock is held. If this field is false then the pre and post methods
+ // take no parameters and the post method is called after the lock is released. This is legacy
+ // behavior.
+ private final boolean scoped;
- public LockTarget(String targetDesc, String pre, String post) {
+ public LockTarget(String targetDesc, String pre, String post, boolean scoped) {
this.targetDesc = targetDesc;
this.pre = pre;
this.post = post;
+ this.scoped = scoped;
+ }
+
+ public LockTarget(String targetDesc, String pre, String post) {
+ this(targetDesc, pre, post, false);
}
public String getTargetDesc() {
@@ -40,7 +54,11 @@ public class LockTarget {
}
public String getPreOwner() {
- return pre.substring(0, pre.lastIndexOf('.'));
+ if (scoped) {
+ return targetDesc.substring(1, targetDesc.length() - 1);
+ } else {
+ return pre.substring(0, pre.lastIndexOf('.'));
+ }
}
public String getPreMethod() {
@@ -52,10 +70,23 @@ public class LockTarget {
}
public String getPostOwner() {
- return post.substring(0, post.lastIndexOf('.'));
+ if (scoped) {
+ return targetDesc.substring(1, targetDesc.length() - 1);
+ } else {
+ return post.substring(0, post.lastIndexOf('.'));
+ }
}
public String getPostMethod() {
return post.substring(post.lastIndexOf('.') + 1);
}
+
+ public boolean getScoped() {
+ return scoped;
+ }
+
+ @Override
+ public String toString() {
+ return targetDesc + ":" + pre + ":" + post;
+ }
}
diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java
index 99d841829132..5df0160abd8c 100644
--- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java
+++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java
@@ -13,10 +13,11 @@
*/
package lockedregioncodeinjection;
-import java.util.List;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.analysis.BasicValue;
+import java.util.List;
+
public class LockTargetState extends BasicValue {
private final List<LockTarget> lockTargets;
@@ -31,4 +32,10 @@ public class LockTargetState extends BasicValue {
public List<LockTarget> getTargets() {
return lockTargets;
}
+
+ @Override
+ public String toString() {
+ return "LockTargetState(" + getType().getDescriptor()
+ + ", " + lockTargets.size() + ")";
+ }
}
diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java
index 828cce72dda9..d22ea2338ff7 100644
--- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java
+++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java
@@ -21,7 +21,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.util.Collections;
+import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
@@ -36,6 +36,7 @@ public class Main {
String legacyTargets = null;
String legacyPreMethods = null;
String legacyPostMethods = null;
+ List<LockTarget> targets = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
if ("-i".equals(args[i].trim())) {
i++;
@@ -52,23 +53,25 @@ public class Main {
} else if ("--post".equals(args[i].trim())) {
i++;
legacyPostMethods = args[i].trim();
+ } else if ("--scoped".equals(args[i].trim())) {
+ i++;
+ targets.add(Utils.getScopedTarget(args[i].trim()));
}
-
}
- // TODO(acleung): Better help message than asserts.
- assert inJar != null;
- assert outJar != null;
+ if (inJar == null) {
+ throw new RuntimeException("missing input jar path");
+ }
+ if (outJar == null) {
+ throw new RuntimeException("missing output jar path");
+ }
assert legacyTargets == null || (legacyPreMethods != null && legacyPostMethods != null);
ZipFile zipSrc = new ZipFile(inJar);
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outJar));
- List<LockTarget> targets = null;
if (legacyTargets != null) {
- targets = Utils.getTargetsFromLegacyJackConfig(legacyTargets, legacyPreMethods,
- legacyPostMethods);
- } else {
- targets = Collections.emptyList();
+ targets.addAll(Utils.getTargetsFromLegacyJackConfig(legacyTargets, legacyPreMethods,
+ legacyPostMethods));
}
Enumeration<? extends ZipEntry> srcEntries = zipSrc.entries();
diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java
index f1e84b1d8a33..bfef10611e6c 100644
--- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java
+++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java
@@ -20,7 +20,7 @@ import java.util.List;
public class Utils {
- public static final int ASM_VERSION = Opcodes.ASM7;
+ public static final int ASM_VERSION = Opcodes.ASM9;
/**
* Reads a comma separated configuration similar to the Jack definition.
@@ -44,4 +44,27 @@ public class Utils {
return config;
}
+
+ /**
+ * Returns a single {@link LockTarget} from a string. The target is a comma-separated list of
+ * the target class, the request method, the release method, and a boolean which is true if this
+ * is a scoped target and false if this is a legacy target. The boolean is optional and
+ * defaults to true.
+ */
+ public static LockTarget getScopedTarget(String arg) {
+ String[] c = arg.split(",");
+ if (c.length == 3) {
+ return new LockTarget(c[0], c[1], c[2], true);
+ } else if (c.length == 4) {
+ if (c[3].equals("true")) {
+ return new LockTarget(c[0], c[1], c[2], true);
+ } else if (c[3].equals("false")) {
+ return new LockTarget(c[0], c[1], c[2], false);
+ } else {
+ System.err.println("illegal target parameter \"" + c[3] + "\"");
+ }
+ }
+ // Fall through
+ throw new RuntimeException("invalid scoped target format");
+ }
}
diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java
index 31fa0bf63416..28f00b9c897a 100644
--- a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java
+++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java
@@ -17,7 +17,10 @@ import org.junit.Assert;
import org.junit.Test;
/**
- * To run the unit tests:
+ * To run the unit tests, first build the two necessary artifacts. Do this explicitly as they are
+ * not generally retained by a normal "build all". After lunching a target:
+ * m lockedregioncodeinjection
+ * m lockedregioncodeinjection_input
*
* <pre>
* <code>
@@ -29,31 +32,25 @@ import org.junit.Test;
* mkdir -p out
* rm -fr out/*
*
- * # Make booster
- * javac -cp lib/asm-7.0_BETA.jar:lib/asm-commons-7.0_BETA.jar:lib/asm-tree-7.0_BETA.jar:lib/asm-analysis-7.0_BETA.jar:lib/guava-21.0.jar src&#47;*&#47;*.java -d out/
- * pushd out
- * jar cfe lockedregioncodeinjection.jar lockedregioncodeinjection.Main *&#47;*.class
- * popd
- *
- * # Make unit tests.
- * javac -cp lib/junit-4.12.jar test&#47;*&#47;*.java -d out/
- *
- * pushd out
- * jar cfe test_input.jar lockedregioncodeinjection.Test *&#47;*.class
- * popd
+ * # Paths to the build artifacts. These assume linux-x86; YMMV.
+ * ROOT=$TOP/out/host/linux-x86
+ * EXE=$ROOT/bin/lockedregioncodeinjection
+ * INPUT=$ROOT/frameworkd/lockedregioncodeinjection_input.jar
*
* # Run tool on unit tests.
- * java -ea -cp lib/asm-7.0_BETA.jar:lib/asm-commons-7.0_BETA.jar:lib/asm-tree-7.0_BETA.jar:lib/asm-analysis-7.0_BETA.jar:lib/guava-21.0.jar:out/lockedregioncodeinjection.jar \
- * lockedregioncodeinjection.Main \
- * -i out/test_input.jar -o out/test_output.jar \
+ * $EXE -i $INPUT -o out/test_output.jar \
* --targets 'Llockedregioncodeinjection/TestTarget;' \
* --pre 'lockedregioncodeinjection/TestTarget.boost' \
* --post 'lockedregioncodeinjection/TestTarget.unboost'
*
* # Run unit tests.
- * java -ea -cp lib/hamcrest-core-1.3.jar:lib/junit-4.12.jar:out/test_output.jar \
+ * java -ea -cp out/test_output.jar \
* org.junit.runner.JUnitCore lockedregioncodeinjection.TestMain
* </code>
+ * OR
+ * <code>
+ * bash test/unit-test.sh
+ * </code>
* </pre>
*/
public class TestMain {
@@ -64,7 +61,9 @@ public class TestMain {
Assert.assertEquals(TestTarget.boostCount, 0);
Assert.assertEquals(TestTarget.unboostCount, 0);
- Assert.assertEquals(TestTarget.unboostCount, 0);
+ Assert.assertEquals(TestTarget.invokeCount, 0);
+ Assert.assertEquals(TestTarget.boostCountLocked, 0);
+ Assert.assertEquals(TestTarget.unboostCountLocked, 0);
synchronized (t) {
Assert.assertEquals(TestTarget.boostCount, 1);
@@ -75,6 +74,8 @@ public class TestMain {
Assert.assertEquals(TestTarget.boostCount, 1);
Assert.assertEquals(TestTarget.unboostCount, 1);
Assert.assertEquals(TestTarget.invokeCount, 1);
+ Assert.assertEquals(TestTarget.boostCountLocked, 0);
+ Assert.assertEquals(TestTarget.unboostCountLocked, 0);
}
@Test
@@ -84,12 +85,16 @@ public class TestMain {
Assert.assertEquals(TestTarget.boostCount, 0);
Assert.assertEquals(TestTarget.unboostCount, 0);
+ Assert.assertEquals(TestTarget.boostCountLocked, 0);
+ Assert.assertEquals(TestTarget.unboostCountLocked, 0);
t.synchronizedCall();
Assert.assertEquals(TestTarget.boostCount, 1);
Assert.assertEquals(TestTarget.unboostCount, 1);
Assert.assertEquals(TestTarget.invokeCount, 1);
+ Assert.assertEquals(TestTarget.boostCountLocked, 0);
+ Assert.assertEquals(TestTarget.unboostCountLocked, 0);
}
@Test
@@ -99,12 +104,16 @@ public class TestMain {
Assert.assertEquals(TestTarget.boostCount, 0);
Assert.assertEquals(TestTarget.unboostCount, 0);
+ Assert.assertEquals(TestTarget.boostCountLocked, 0);
+ Assert.assertEquals(TestTarget.unboostCountLocked, 0);
t.synchronizedCallReturnInt();
Assert.assertEquals(TestTarget.boostCount, 1);
Assert.assertEquals(TestTarget.unboostCount, 1);
Assert.assertEquals(TestTarget.invokeCount, 1);
+ Assert.assertEquals(TestTarget.boostCountLocked, 0);
+ Assert.assertEquals(TestTarget.unboostCountLocked, 0);
}
@Test
@@ -253,4 +262,125 @@ public class TestMain {
Assert.assertEquals(TestTarget.invokeCount, 1);
}
+ @Test
+ public void testScopedTarget() {
+ TestScopedTarget target = new TestScopedTarget();
+ Assert.assertEquals(0, target.scopedLock().mLevel);
+
+ synchronized (target.scopedLock()) {
+ Assert.assertEquals(1, target.scopedLock().mLevel);
+ }
+ Assert.assertEquals(0, target.scopedLock().mLevel);
+
+ synchronized (target.scopedLock()) {
+ synchronized (target.scopedLock()) {
+ Assert.assertEquals(2, target.scopedLock().mLevel);
+ }
+ }
+ Assert.assertEquals(0, target.scopedLock().mLevel);
+ }
+
+ @Test
+ public void testScopedExceptionHandling() {
+ TestScopedTarget target = new TestScopedTarget();
+ Assert.assertEquals(0, target.scopedLock().mLevel);
+
+ boolean handled;
+
+ // 1: an exception inside the block properly releases the lock.
+ handled = false;
+ try {
+ synchronized (target.scopedLock()) {
+ Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
+ Assert.assertEquals(1, target.scopedLock().mLevel);
+ throw new RuntimeException();
+ }
+ } catch (RuntimeException e) {
+ Assert.assertEquals(0, target.scopedLock().mLevel);
+ handled = true;
+ }
+ Assert.assertEquals(0, target.scopedLock().mLevel);
+ Assert.assertEquals(true, handled);
+ // Just verify that the lock can still be taken
+ Assert.assertEquals(false, Thread.holdsLock(target.scopedLock()));
+
+ // 2: An exception inside the monitor enter function
+ handled = false;
+ target.throwOnEnter(true);
+ try {
+ synchronized (target.scopedLock()) {
+ // The exception was thrown inside monitorEnter(), so the code should
+ // never reach this point.
+ Assert.assertEquals(0, 1);
+ }
+ } catch (RuntimeException e) {
+ Assert.assertEquals(0, target.scopedLock().mLevel);
+ handled = true;
+ }
+ Assert.assertEquals(0, target.scopedLock().mLevel);
+ Assert.assertEquals(true, handled);
+ // Just verify that the lock can still be taken
+ Assert.assertEquals(false, Thread.holdsLock(target.scopedLock()));
+
+ // 3: An exception inside the monitor exit function
+ handled = false;
+ target.throwOnEnter(true);
+ try {
+ synchronized (target.scopedLock()) {
+ Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
+ Assert.assertEquals(1, target.scopedLock().mLevel);
+ }
+ } catch (RuntimeException e) {
+ Assert.assertEquals(0, target.scopedLock().mLevel);
+ handled = true;
+ }
+ Assert.assertEquals(0, target.scopedLock().mLevel);
+ Assert.assertEquals(true, handled);
+ // Just verify that the lock can still be taken
+ Assert.assertEquals(false, Thread.holdsLock(target.scopedLock()));
+ }
+
+ // Provide an in-class type conversion for the scoped target.
+ private Object untypedLock(TestScopedTarget target) {
+ return target.scopedLock();
+ }
+
+ @Test
+ public void testScopedLockTyping() {
+ TestScopedTarget target = new TestScopedTarget();
+ Assert.assertEquals(target.scopedLock().mLevel, 0);
+
+ // Scoped lock injection works on the static type of an object. In general, it is
+ // a very bad idea to do type conversion on scoped locks, but the general rule is
+ // that conversions within a single method are recognized by the lock injection
+ // tool and injection occurs. Conversions outside a single method are not
+ // recognized and injection does not occur.
+
+ // 1. Conversion occurs outside the class. The visible type of the lock is Object
+ // in this block, so no injection takes place on 'untypedLock', even though the
+ // dynamic type is TestScopedLock.
+ synchronized (target.untypedLock()) {
+ Assert.assertEquals(0, target.scopedLock().mLevel);
+ Assert.assertEquals(true, target.untypedLock() instanceof TestScopedLock);
+ Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
+ }
+
+ // 2. Conversion occurs inside the class but in another method. The visible type
+ // of the lock is Object in this block, so no injection takes place on
+ // 'untypedLock', even though the dynamic type is TestScopedLock.
+ synchronized (untypedLock(target)) {
+ Assert.assertEquals(0, target.scopedLock().mLevel);
+ Assert.assertEquals(true, target.untypedLock() instanceof TestScopedLock);
+ Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
+ }
+
+ // 3. Conversion occurs inside the method. The compiler can determine the type of
+ // the lock within a single function, so injection does take place here.
+ Object untypedLock = target.scopedLock();
+ synchronized (untypedLock) {
+ Assert.assertEquals(1, target.scopedLock().mLevel);
+ Assert.assertEquals(true, untypedLock instanceof TestScopedLock);
+ Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
+ }
+ }
}
diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java
new file mode 100644
index 000000000000..7441d2b1da48
--- /dev/null
+++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 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 lockedregioncodeinjection;
+
+public class TestScopedLock {
+ public int mEntered = 0;
+ public int mExited = 0;
+
+ public int mLevel = 0;
+ private final TestScopedTarget mTarget;
+
+ TestScopedLock(TestScopedTarget target) {
+ mTarget = target;
+ }
+
+ void monitorEnter() {
+ mLevel++;
+ mEntered++;
+ mTarget.enter(mLevel);
+ }
+
+ void monitorExit() {
+ mLevel--;
+ mExited++;
+ mTarget.exit(mLevel);
+ }
+}
diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java
new file mode 100644
index 000000000000..c80975e9c6b3
--- /dev/null
+++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 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 lockedregioncodeinjection;
+
+public class TestScopedTarget {
+
+ public final TestScopedLock mLock;;
+
+ private boolean mNextEnterThrows = false;
+ private boolean mNextExitThrows = false;
+
+ TestScopedTarget() {
+ mLock = new TestScopedLock(this);
+ }
+
+ TestScopedLock scopedLock() {
+ return mLock;
+ }
+
+ Object untypedLock() {
+ return mLock;
+ }
+
+ void enter(int level) {
+ if (mNextEnterThrows) {
+ mNextEnterThrows = false;
+ throw new RuntimeException();
+ }
+ }
+
+ void exit(int level) {
+ if (mNextExitThrows) {
+ mNextExitThrows = false;
+ throw new RuntimeException();
+ }
+ }
+
+ void throwOnEnter(boolean b) {
+ mNextEnterThrows = b;
+ }
+
+ void throwOnExit(boolean b) {
+ mNextExitThrows = b;
+ }
+}
diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java
index d1c8f340a598..e3ba6a77e3b7 100644
--- a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java
+++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java
@@ -19,8 +19,17 @@ public class TestTarget {
public static int invokeCount = 0;
public static boolean nextUnboostThrows = false;
+ // If this is not null, then this is the lock under test. The lock must not be held when boost()
+ // or unboost() are called.
+ public static Object mLock = null;
+ public static int boostCountLocked = 0;
+ public static int unboostCountLocked = 0;
+
public static void boost() {
boostCount++;
+ if (mLock != null && Thread.currentThread().holdsLock(mLock)) {
+ boostCountLocked++;
+ }
}
public static void unboost() {
@@ -29,6 +38,9 @@ public class TestTarget {
throw new RuntimeException();
}
unboostCount++;
+ if (mLock != null && Thread.currentThread().holdsLock(mLock)) {
+ unboostCountLocked++;
+ }
}
public static void invoke() {
diff --git a/tools/locked_region_code_injection/test/manifest.txt b/tools/locked_region_code_injection/test/manifest.txt
new file mode 100644
index 000000000000..2314c188721c
--- /dev/null
+++ b/tools/locked_region_code_injection/test/manifest.txt
@@ -0,0 +1 @@
+Main-Class: org.junit.runner.JUnitCore
diff --git a/tools/locked_region_code_injection/test/unit-test.sh b/tools/locked_region_code_injection/test/unit-test.sh
new file mode 100755
index 000000000000..9fa6f39af14a
--- /dev/null
+++ b/tools/locked_region_code_injection/test/unit-test.sh
@@ -0,0 +1,98 @@
+#! /bin/bash
+#
+
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+# in compliance with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License
+# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+# or implied. See the License for the specific language governing permissions and limitations under
+# the License.
+
+# This script runs the tests for the lockedregioninjectioncode. See
+# TestMain.java for the invocation. The script expects that a full build has
+# already been done and artifacts are in $TOP/out.
+
+# Compute the default top of the workspace. The following code copies the
+# strategy of croot. (croot cannot be usd directly because it is a function and
+# functions are not carried over into subshells.) This gives the correct answer
+# if run from inside a workspace. If run from outside a workspace, supply TOP
+# on the command line.
+TOPFILE=build/make/core/envsetup.mk
+TOP=$(dirname $(realpath $0))
+while [[ ! $TOP = / && ! -f $TOP/$TOPFILE ]]; do
+ TOP=$(dirname $TOP)
+done
+# TOP is "/" if this script is located outside a workspace.
+
+# If the user supplied a top directory, use it instead
+if [[ -n $1 ]]; then
+ TOP=$1
+ shift
+fi
+if [[ -z $TOP || $TOP = / ]]; then
+ echo "usage: $0 <workspace-root>"
+ exit 1
+elif [[ ! -d $TOP ]]; then
+ echo "$TOP is not a directory"
+ exit 1
+elif [[ ! -d $TOP/prebuilts/misc/common ]]; then
+ echo "$TOP does not look like w workspace"
+ exit 1
+fi
+echo "Using workspace $TOP"
+
+# Pick up the current java compiler. The lunch target is not very important,
+# since most, if not all, will use the same host binaries.
+pushd $TOP > /dev/null
+. build/envsetup.sh > /dev/null 2>&1
+lunch redfin-userdebug > /dev/null 2>&1
+popd > /dev/null
+
+# Bail on any error
+set -o pipefail
+trap 'exit 1' ERR
+
+# Create the two sources
+pushd $TOP > /dev/null
+m lockedregioncodeinjection
+m lockedregioncodeinjection_input
+popd > /dev/null
+
+# Create a temporary directory outside of the workspace.
+OUT=$TOP/out/host/test/lockedregioncodeinjection
+echo
+
+# Clean the directory
+if [[ -d $OUT ]]; then rm -r $OUT; fi
+mkdir -p $OUT
+
+ROOT=$TOP/out/host/linux-x86
+EXE=$ROOT/bin/lockedregioncodeinjection
+INP=$ROOT/framework/lockedregioncodeinjection_input.jar
+
+# Run tool on unit tests.
+$EXE \
+ -i $INP -o $OUT/test_output.jar \
+ --targets 'Llockedregioncodeinjection/TestTarget;' \
+ --pre 'lockedregioncodeinjection/TestTarget.boost' \
+ --post 'lockedregioncodeinjection/TestTarget.unboost' \
+ --scoped 'Llockedregioncodeinjection/TestScopedLock;,monitorEnter,monitorExit'
+
+# Run unit tests.
+java -ea -cp $OUT/test_output.jar \
+ org.junit.runner.JUnitCore lockedregioncodeinjection.TestMain
+
+# Extract the class files and decompile them for possible post-analysis.
+pushd $OUT > /dev/null
+jar -x --file test_output.jar lockedregioncodeinjection
+for class in lockedregioncodeinjection/*.class; do
+ javap -c -v $class > ${class%.class}.asm
+done
+popd > /dev/null
+
+echo "artifacts are in $OUT"
diff --git a/tools/preload-check/AndroidTest.xml b/tools/preload-check/AndroidTest.xml
index a0645d5b1051..d486f0f914c6 100644
--- a/tools/preload-check/AndroidTest.xml
+++ b/tools/preload-check/AndroidTest.xml
@@ -14,7 +14,7 @@
limitations under the License.
-->
<configuration description="Config for PreloadCheck">
- <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
<option name="cleanup" value="true" />
<option name="push" value="preload-check-device.jar->/data/local/tmp/preload-check-device.jar" />
</target_preparer>
diff --git a/tools/preload-check/OWNERS b/tools/preload-check/OWNERS
new file mode 100644
index 000000000000..e71c7332ca90
--- /dev/null
+++ b/tools/preload-check/OWNERS
@@ -0,0 +1 @@
+include /ZYGOTE_OWNERS
diff --git a/tools/preload/loadclass/LoadClass.java b/tools/preload/loadclass/LoadClass.java
index a71b6a8b145e..3f6658ab8c65 100644
--- a/tools/preload/loadclass/LoadClass.java
+++ b/tools/preload/loadclass/LoadClass.java
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-import android.util.Log;
import android.os.Debug;
+import android.util.Log;
/**
* Loads a class, runs the garbage collector, and prints showmap output.
@@ -28,7 +28,7 @@ class LoadClass {
System.loadLibrary("android_runtime");
if (registerNatives() < 0) {
- throw new RuntimeException("Error registering natives.");
+ throw new RuntimeException("Error registering natives.");
}
Debug.startAllocCounting();
@@ -46,7 +46,7 @@ class LoadClass {
}
}
- System.gc();
+ Runtime.getRuntime().gc();
int allocCount = Debug.getGlobalAllocCount();
int allocSize = Debug.getGlobalAllocSize();
@@ -73,7 +73,7 @@ class LoadClass {
response.append(',').append(freedCount);
response.append(',').append(freedSize);
response.append(',').append(nativeHeapSize);
-
+
System.out.println(response.toString());
}
diff --git a/tools/processors/intdef_mappings/Android.bp b/tools/processors/intdef_mappings/Android.bp
index 82a5dac21160..7059c52ddc76 100644
--- a/tools/processors/intdef_mappings/Android.bp
+++ b/tools/processors/intdef_mappings/Android.bp
@@ -7,27 +7,33 @@ package {
default_applicable_licenses: ["frameworks_base_license"],
}
-java_plugin {
- name: "intdef-annotation-processor",
-
- processor_class: "android.processor.IntDefProcessor",
+java_library_host {
+ name: "libintdef-annotation-processor",
srcs: [
":framework-annotations",
"src/**/*.java",
- "src/**/*.kt"
+ "src/**/*.kt",
],
use_tools_jar: true,
}
+java_plugin {
+ name: "intdef-annotation-processor",
+
+ processor_class: "android.processor.IntDefProcessor",
+
+ static_libs: ["libintdef-annotation-processor"],
+}
+
java_test_host {
name: "intdef-annotation-processor-test",
srcs: [
"test/**/*.java",
- "test/**/*.kt"
- ],
+ "test/**/*.kt",
+ ],
java_resource_dirs: ["test/resources"],
static_libs: [
@@ -35,7 +41,7 @@ java_test_host {
"truth-prebuilt",
"junit",
"guava",
- "intdef-annotation-processor"
+ "libintdef-annotation-processor",
],
test_suites: ["general-tests"],
diff --git a/tools/processors/staledataclass/Android.bp b/tools/processors/staledataclass/Android.bp
index 1e5097662a0a..2169c49a91a5 100644
--- a/tools/processors/staledataclass/Android.bp
+++ b/tools/processors/staledataclass/Android.bp
@@ -22,17 +22,13 @@ java_plugin {
static_libs: [
"codegen-version-info",
],
- // The --add-modules/exports flags below don't work for kotlinc yet, so pin this module to Java language level 8 (see b/139342589):
- java_version: "1.8",
- openjdk9: {
- javacflags: [
- "--add-modules=jdk.compiler",
- "--add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
- "--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED",
- "--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
- "--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
- ],
- },
+ javacflags: [
+ "--add-modules=jdk.compiler",
+ "--add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
+ "--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED",
+ "--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
+ "--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
+ ],
use_tools_jar: true,
}
diff --git a/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt b/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt
index 2e60f64b21e8..1cef5b0c8dfb 100644
--- a/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt
+++ b/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE")
package android.processor.staledataclass
@@ -97,7 +98,7 @@ class StaleDataclassProcessor: AbstractProcessor() {
private fun elemToString(elem: Element): String {
return buildString {
- append(elem.modifiers.joinToString(" ") { it.name.toLowerCase() })
+ append(elem.modifiers.joinToString(" ") { it.name.lowercase() })
append(" ")
append(elem.annotationMirrors.joinToString(" ", transform = { annotationToString(it) }))
append(" ")
diff --git a/tools/processors/view_inspector/Android.bp b/tools/processors/view_inspector/Android.bp
index ea9974f06a64..877a1d2b5fb3 100644
--- a/tools/processors/view_inspector/Android.bp
+++ b/tools/processors/view_inspector/Android.bp
@@ -7,10 +7,8 @@ package {
default_applicable_licenses: ["frameworks_base_license"],
}
-java_plugin {
- name: "view-inspector-annotation-processor",
-
- processor_class: "android.processor.view.inspector.PlatformInspectableProcessor",
+java_library_host {
+ name: "libview-inspector-annotation-processor",
srcs: ["src/java/**/*.java"],
java_resource_dirs: ["src/resources"],
@@ -23,6 +21,16 @@ java_plugin {
use_tools_jar: true,
}
+java_plugin {
+ name: "view-inspector-annotation-processor",
+
+ processor_class: "android.processor.view.inspector.PlatformInspectableProcessor",
+
+ static_libs: [
+ "libview-inspector-annotation-processor",
+ ],
+}
+
java_test_host {
name: "view-inspector-annotation-processor-test",
@@ -32,7 +40,7 @@ java_test_host {
static_libs: [
"junit",
"guava",
- "view-inspector-annotation-processor"
+ "libview-inspector-annotation-processor",
],
test_suites: ["general-tests"],
diff --git a/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt b/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt
index a52c8042582b..451e514b8b33 100644
--- a/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt
@@ -29,7 +29,7 @@ object CodeUtils {
*/
fun hash(position: String, messageString: String, logLevel: LogLevel, logGroup: LogGroup): Int {
return (position + messageString + logLevel.name + logGroup.name)
- .map { c -> c.toInt() }.reduce { h, c -> h * 31 + c }
+ .map { c -> c.code }.reduce { h, c -> h * 31 + c }
}
fun checkWildcardStaticImported(code: CompilationUnit, className: String, fileName: String) {
diff --git a/tools/sdkparcelables/Android.bp b/tools/sdkparcelables/Android.bp
index 6ebacd8a0b14..6503a1f3a5f8 100644
--- a/tools/sdkparcelables/Android.bp
+++ b/tools/sdkparcelables/Android.bp
@@ -14,7 +14,7 @@ java_binary_host {
"src/**/*.kt",
],
static_libs: [
- "asm-9.2",
+ "ow2-asm",
],
}
diff --git a/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt b/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt
index 0fb062f280e3..0b619488c49c 100644
--- a/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt
+++ b/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt
@@ -39,7 +39,7 @@ fun main(args: Array<String>) {
kotlin.system.exitProcess(2)
}
- val ancestorCollector = AncestorCollector(Opcodes.ASM7, null)
+ val ancestorCollector = AncestorCollector(Opcodes.ASM9, null)
for (entry in zipFile.entries()) {
if (entry.name.endsWith(".class")) {
diff --git a/tools/stringslint/stringslint.py b/tools/stringslint/stringslint.py
deleted file mode 100644
index 15088fc81e88..000000000000
--- a/tools/stringslint/stringslint.py
+++ /dev/null
@@ -1,234 +0,0 @@
-#!/usr/bin/env python3
-#-*- coding: utf-8 -*-
-
-# Copyright (C) 2018 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.
-
-"""
-Enforces common Android string best-practices. It ignores lint messages from
-a previous strings file, if provided.
-
-Usage: stringslint.py strings.xml
-Usage: stringslint.py strings.xml old_strings.xml
-
-In general:
-* Errors signal issues that must be fixed before submitting, and are only
- used when there are no false-positives.
-* Warnings signal issues that might need to be fixed, but need manual
- inspection due to risk of false-positives.
-* Info signal issues that should be fixed to match best-practices, such
- as providing comments to aid translation.
-"""
-
-import re, sys, codecs
-import lxml.etree as ET
-
-BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
-
-def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False):
- # manually derived from http://en.wikipedia.org/wiki/ANSI_escape_code#Codes
- codes = []
- if reset: codes.append("0")
- else:
- if not fg is None: codes.append("3%d" % (fg))
- if not bg is None:
- if not bright: codes.append("4%d" % (bg))
- else: codes.append("10%d" % (bg))
- if bold: codes.append("1")
- elif dim: codes.append("2")
- else: codes.append("22")
- return "\033[%sm" % (";".join(codes))
-
-warnings = None
-
-def warn(tag, msg, actual, expected, color=YELLOW):
- global warnings
- key = "%s:%d" % (tag.attrib["name"], hash(msg))
- value = "%sLine %d: '%s':%s %s" % (format(fg=color, bold=True),
- tag.sourceline,
- tag.attrib["name"],
- format(reset=True),
- msg)
- if not actual is None: value += "\n\tActual: %s%s%s" % (format(dim=True),
- actual,
- format(reset=True))
- if not expected is None: value += "\n\tExample: %s%s%s" % (format(dim=True),
- expected,
- format(reset=True))
- warnings[key] = value
-
-
-def error(tag, msg, actual, expected):
- warn(tag, msg, actual, expected, RED)
-
-def info(tag, msg, actual, expected):
- warn(tag, msg, actual, expected, CYAN)
-
-# Escaping logic borrowed from https://stackoverflow.com/a/24519338
-ESCAPE_SEQUENCE_RE = re.compile(r'''
- ( \\U........ # 8-digit hex escapes
- | \\u.... # 4-digit hex escapes
- | \\x.. # 2-digit hex escapes
- | \\[0-7]{1,3} # Octal escapes
- | \\N\{[^}]+\} # Unicode characters by name
- | \\[\\'"abfnrtv] # Single-character escapes
- )''', re.UNICODE | re.VERBOSE)
-
-def decode_escapes(s):
- def decode_match(match):
- return codecs.decode(match.group(0), 'unicode-escape')
-
- s = re.sub(r"\n\s*", " ", s)
- s = ESCAPE_SEQUENCE_RE.sub(decode_match, s)
- s = re.sub(r"%(\d+\$)?[a-z]", "____", s)
- s = re.sub(r"\^\d+", "____", s)
- s = re.sub(r"<br/?>", "\n", s)
- s = re.sub(r"</?[a-z]+>", "", s)
- return s
-
-def sample_iter(tag):
- if not isinstance(tag, ET._Comment) and re.match("{.*xliff.*}g", tag.tag) and "example" in tag.attrib:
- yield tag.attrib["example"]
- elif tag.text:
- yield decode_escapes(tag.text)
- for e in tag:
- for v in sample_iter(e):
- yield v
- if e.tail:
- yield decode_escapes(e.tail)
-
-def lint(path):
- global warnings
- warnings = {}
-
- with open(path) as f:
- raw = f.read()
- if len(raw.strip()) == 0:
- return warnings
- tree = ET.fromstring(bytes(raw, encoding='utf-8'))
- root = tree #tree.getroot()
-
- last_comment = None
- for child in root:
- # TODO: handle plurals
- if isinstance(child, ET._Comment):
- last_comment = child
- elif child.tag == "string":
- # We always consume comment
- comment = last_comment
- last_comment = None
-
- # Prepare string for analysis
- text = "".join(child.itertext())
- sample = "".join(sample_iter(child)).strip().strip("'\"")
-
- # Validate comment
- if comment is None:
- info(child, "Missing string comment to aid translation",
- None, None)
- continue
- if "do not translate" in comment.text.lower():
- continue
- if "translatable" in child.attrib and child.attrib["translatable"].lower() == "false":
- continue
-
- misspelled_attributes = [
- ("translateable", "translatable"),
- ]
- for misspelling, expected in misspelled_attributes:
- if misspelling in child.attrib:
- error(child, "Misspelled <string> attribute.", misspelling, expected)
-
- limit = re.search("CHAR[ _-]LIMIT=(\d+|NONE|none)", comment.text)
- if limit is None:
- info(child, "Missing CHAR LIMIT to aid translation",
- repr(comment), "<!-- Description of string [CHAR LIMIT=32] -->")
- elif re.match("\d+", limit.group(1)):
- limit = int(limit.group(1))
- if len(sample) > limit:
- warn(child, "Expanded string length is larger than CHAR LIMIT",
- sample, None)
-
- # Look for common mistakes/substitutions
- if "'" in text:
- error(child, "Turned quotation mark glyphs are more polished",
- text, "This doesn\u2019t need to \u2018happen\u2019 today")
- if '"' in text and not text.startswith('"') and text.endswith('"'):
- error(child, "Turned quotation mark glyphs are more polished",
- text, "This needs to \u201chappen\u201d today")
- if "..." in text:
- error(child, "Ellipsis glyph is more polished",
- text, "Loading\u2026")
- if "wi-fi" in text.lower():
- error(child, "Non-breaking glyph is more polished",
- text, "Wi\u2011Fi")
- if "wifi" in text.lower():
- error(child, "Using non-standard spelling",
- text, "Wi\u2011Fi")
- if re.search("\d-\d", text):
- warn(child, "Ranges should use en dash glyph",
- text, "You will find this material in chapters 8\u201312")
- if "--" in text:
- warn(child, "Phrases should use em dash glyph",
- text, "Upon discovering errors\u2014all 124 of them\u2014they recalled.")
- if ". " in text:
- warn(child, "Only use single space between sentences",
- text, "First idea. Second idea.")
- if re.match(r"^[A-Z\s]{5,}$", text):
- warn(child, "Actions should use android:textAllCaps in layout; ignore if acronym",
- text, "Refresh data")
- if " phone " in text and "product" not in child.attrib:
- warn(child, "Strings mentioning phones should have variants for tablets",
- text, None)
-
- # When more than one substitution, require indexes
- if len(re.findall("%[^%]", text)) > 1:
- if len(re.findall("%[^\d]", text)) > 0:
- error(child, "Substitutions must be indexed",
- text, "Add %1$s to %2$s")
-
- # Require xliff substitutions
- for gc in child.iter():
- badsub = False
- if gc.tail and re.search("%[^%]", gc.tail): badsub = True
- if re.match("{.*xliff.*}g", gc.tag):
- if "id" not in gc.attrib:
- error(child, "Substitutions must define id attribute",
- None, "<xliff:g id=\"domain\" example=\"example.com\">%1$s</xliff:g>")
- if "example" not in gc.attrib:
- error(child, "Substitutions must define example attribute",
- None, "<xliff:g id=\"domain\" example=\"example.com\">%1$s</xliff:g>")
- else:
- if gc.text and re.search("%[^%]", gc.text): badsub = True
- if badsub:
- error(child, "Substitutions must be inside xliff tags",
- text, "<xliff:g id=\"domain\" example=\"example.com\">%1$s</xliff:g>")
-
- return warnings
-
-if len(sys.argv) > 2:
- before = lint(sys.argv[2])
-else:
- before = {}
-after = lint(sys.argv[1])
-
-for b in before:
- if b in after:
- del after[b]
-
-if len(after) > 0:
- for a in sorted(after.keys()):
- print(after[a])
- print()
- sys.exit(1)
diff --git a/tools/stringslint/stringslint_sha.sh b/tools/stringslint/stringslint_sha.sh
index bd0569873197..009a1f284bf0 100755
--- a/tools/stringslint/stringslint_sha.sh
+++ b/tools/stringslint/stringslint_sha.sh
@@ -1,5 +1,3 @@
#!/bin/bash
-LOCAL_DIR="$( dirname ${BASH_SOURCE} )"
-git show --name-only --pretty=format: $1 | grep values/strings.xml | while read file; do
- python3 $LOCAL_DIR/stringslint.py <(git show $1:$file) <(git show $1^:$file)
-done
+
+# NOTE: this script has been deprecated and replaced by AyeAye checks directly in Gerrit
diff --git a/tools/traceinjection/Android.bp b/tools/traceinjection/Android.bp
index 39d1b1c2defd..d627fb99d882 100644
--- a/tools/traceinjection/Android.bp
+++ b/tools/traceinjection/Android.bp
@@ -12,11 +12,11 @@ java_binary_host {
manifest: "manifest.txt",
srcs: ["src/**/*.java"],
static_libs: [
- "asm-9.2",
- "asm-commons-9.2",
- "asm-tree-9.2",
- "asm-analysis-9.2",
- "guava-21.0",
+ "ow2-asm",
+ "ow2-asm-commons",
+ "ow2-asm-tree",
+ "ow2-asm-analysis",
+ "guava",
],
}
diff --git a/tools/traceinjection/src/com/android/traceinjection/TraceInjectionClassVisitor.java b/tools/traceinjection/src/com/android/traceinjection/TraceInjectionClassVisitor.java
index 863f976b8aff..67c5561f348d 100644
--- a/tools/traceinjection/src/com/android/traceinjection/TraceInjectionClassVisitor.java
+++ b/tools/traceinjection/src/com/android/traceinjection/TraceInjectionClassVisitor.java
@@ -28,7 +28,7 @@ public class TraceInjectionClassVisitor extends ClassVisitor {
private final TraceInjectionConfiguration mParams;
public TraceInjectionClassVisitor(ClassVisitor classVisitor,
TraceInjectionConfiguration params) {
- super(Opcodes.ASM7, classVisitor);
+ super(Opcodes.ASM9, classVisitor);
mParams = params;
}
diff --git a/tools/traceinjection/src/com/android/traceinjection/TraceInjectionMethodAdapter.java b/tools/traceinjection/src/com/android/traceinjection/TraceInjectionMethodAdapter.java
index c2bbddcb5668..91e987dc72ee 100644
--- a/tools/traceinjection/src/com/android/traceinjection/TraceInjectionMethodAdapter.java
+++ b/tools/traceinjection/src/com/android/traceinjection/TraceInjectionMethodAdapter.java
@@ -61,7 +61,7 @@ public class TraceInjectionMethodAdapter extends AdviceAdapter {
public TraceInjectionMethodAdapter(MethodVisitor methodVisitor, int access,
String name, String descriptor, TraceInjectionConfiguration params) {
- super(Opcodes.ASM7, methodVisitor, access, name, descriptor);
+ super(Opcodes.ASM9, methodVisitor, access, name, descriptor);
mParams = params;
mIsConstructor = "<init>".equals(name);
}
@@ -157,7 +157,7 @@ public class TraceInjectionMethodAdapter extends AdviceAdapter {
class TracingAnnotationVisitor extends AnnotationVisitor {
TracingAnnotationVisitor(AnnotationVisitor annotationVisitor) {
- super(Opcodes.ASM7, annotationVisitor);
+ super(Opcodes.ASM9, annotationVisitor);
}
@Override
diff --git a/tools/validatekeymaps/OWNERS b/tools/validatekeymaps/OWNERS
index 0313a40f7270..4c20c4dc9d35 100644
--- a/tools/validatekeymaps/OWNERS
+++ b/tools/validatekeymaps/OWNERS
@@ -1,2 +1 @@
-michaelwr@google.com
-svv@google.com
+include /INPUT_OWNERS