Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ data class DuckDuckGoColors(
val accentYellow: Color,
val ripple: Color,
val text: DuckDuckGoTextColors,
val textField: DuckDuckGoTextFieldColors,
val icons: DuckDuckGoIconColors,
val isDark: Boolean, // TODO we'll need to do an exploration into using the app pref for Theme switching
)

Expand All @@ -60,6 +62,16 @@ data class DuckDuckGoTextColors(
val omnibarHighlight: Color,
)

@Immutable
data class DuckDuckGoTextFieldColors(
val borders: Color,
)

@Immutable
data class DuckDuckGoIconColors(
val primary: Color,
)

@SuppressLint("ComposeCompositionLocalUsage")
val LocalDuckDuckGoColors = staticCompositionLocalOf<DuckDuckGoColors> {
error("No DuckDuckGoColors provided")
Expand All @@ -84,6 +96,7 @@ val Black = Color(0xFF000000)

//region White color variants
val White84 = Color(0xD6FFFFFF)
val White78 = Color(0xC7FFFFFF)
val White60 = Color(0x99FFFFFF)
val White48 = Color(0x7AFFFFFF)
val White40 = Color(0x66FFFFFF)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ object DuckDuckGoTheme {
@ReadOnlyComposable
get() = colors.text

val iconColors: DuckDuckGoIconColors
@Composable
@ReadOnlyComposable
get() = colors.icons

val shapes
@Composable
@ReadOnlyComposable
Expand Down Expand Up @@ -95,6 +100,12 @@ fun DuckDuckGoTheme(
logoTitle = Gray85,
omnibarHighlight = colorResource(R.color.blue50_20),
),
textField = DuckDuckGoTextFieldColors(
borders = colorResource(R.color.black30),
),
icons = DuckDuckGoIconColors(
primary = Black84,
),
isDark = false,
)

Expand All @@ -121,6 +132,12 @@ fun DuckDuckGoTheme(
logoTitle = White,
omnibarHighlight = colorResource(R.color.blue30_20),
),
textField = DuckDuckGoTextFieldColors(
borders = colorResource(R.color.white30),
),
icons = DuckDuckGoIconColors(
primary = White78,
),
isDark = true,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import com.duckduckgo.lint.strings.PlaceholderDetector.Companion.PLACEHOLDER_MIS
import com.duckduckgo.lint.ui.ColorAttributeInXmlDetector.Companion.INVALID_COLOR_ATTRIBUTE
import com.duckduckgo.lint.ui.DaxButtonStylingDetector.Companion.INVALID_DAX_BUTTON_PROPERTY
import com.duckduckgo.lint.ui.DaxTextColorUsageDetector.Companion.INVALID_DAX_TEXT_COLOR_USAGE
import com.duckduckgo.lint.ui.DaxTextFieldTrailingIconDetector.Companion.INVALID_DAX_TEXT_FIELD_TRAILING_ICON_USAGE
import com.duckduckgo.lint.ui.DaxSecureTextFieldTrailingIconDetector.Companion.INVALID_DAX_SECURE_TEXT_FIELD_TRAILING_ICON_USAGE
import com.duckduckgo.lint.ui.DaxTextViewStylingDetector.Companion.INVALID_DAX_TEXT_VIEW_PROPERTY
import com.duckduckgo.lint.ui.DeprecatedAndroidWidgetsUsedInXmlDetector.Companion.DEPRECATED_WIDGET_IN_XML
import com.duckduckgo.lint.ui.MissingDividerDetector.Companion.MISSING_HORIZONTAL_DIVIDER
Expand Down Expand Up @@ -86,6 +88,8 @@ class DuckDuckGoIssueRegistry : IssueRegistry() {
NO_COMPOSE_VIEW_USAGE,
NO_SET_CONTENT_USAGE,
INVALID_DAX_TEXT_COLOR_USAGE,
INVALID_DAX_TEXT_FIELD_TRAILING_ICON_USAGE,
INVALID_DAX_SECURE_TEXT_FIELD_TRAILING_ICON_USAGE,

).plus(WebViewCompatApisUsageDetector.issues)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* 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.duckduckgo.lint.ui

import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Category.Companion.CUSTOM_LINT_CHECKS
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.TextFormat
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.getParameterForArgument
import java.util.EnumSet

@Suppress("UnstableApiUsage")
class DaxSecureTextFieldTrailingIconDetector : Detector(), SourceCodeScanner {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️


override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)

override fun createUastHandler(context: JavaContext): UElementHandler = DaxSecureTextFieldCallHandler(context)

internal class DaxSecureTextFieldCallHandler(private val context: JavaContext) : UElementHandler() {
override fun visitCallExpression(node: UCallExpression) {
val methodName = node.methodName

if (methodName == "DaxSecureTextField") {
checkTrailingIconParameter(node)
}
}

private fun checkTrailingIconParameter(node: UCallExpression) {
// Find the 'trailingIcon' parameter
val trailingIconArgument = node.valueArguments.find { arg ->
val parameterName = node.getParameterForArgument(arg)?.name
parameterName == "trailingIcon"
} ?: return // No 'trailingIcon' parameter provided which is fine

// Check if the trailingIcon uses an invalid composable
if (isInvalidComposable(trailingIconArgument)) {
reportInvalidComposableUsage(trailingIconArgument)
}
}

private fun isInvalidComposable(argument: org.jetbrains.uast.UExpression): Boolean {
val source = argument.sourcePsi?.text ?: return false

// Only DaxTextFieldTrailingIcon should be used in the trailingIcon parameter
// If the source doesn't contain it, then it's invalid
return !source.contains("DaxTextFieldTrailingIcon")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably get member list declared in the scope object class and check if the source contains one member of this list? This will allow us to avoid to edit this linter rule every time we need to edit the scope.

}

private fun reportInvalidComposableUsage(arg: org.jetbrains.uast.UExpression) {
context.report(
issue = INVALID_DAX_SECURE_TEXT_FIELD_TRAILING_ICON_USAGE,
location = context.getLocation(arg),
message = INVALID_DAX_SECURE_TEXT_FIELD_TRAILING_ICON_USAGE.getExplanation(TextFormat.RAW),
)
}
}

companion object {
val INVALID_DAX_SECURE_TEXT_FIELD_TRAILING_ICON_USAGE = Issue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we improve our implementation to be aware about all member in the scope class, we'll need to edit this issue

.create(
id = "InvalidDaxSecureTextFieldTrailingIconUsage",
briefDescription = "DaxSecureTextField trailingIcon parameter should use DaxTextFieldTrailingIcon",
explanation = """
Use DaxTextFieldTrailingIcon instead of arbitrary composables for the trailingIcon parameter to maintain design system consistency.
Example:
DaxSecureTextField(
state = state,
isPasswordVisible = isPasswordVisible,
onShowHidePasswordIconClick = { /* toggle visibility */ },
trailingIcon = {
DaxTextFieldTrailingIcon(
painter = painterResource(R.drawable.ic_copy_24),
contentDescription = stringResource(R.string.icon_description)
)
}
)
This ensures consistent styling, spacing, and behavior across all text field icons in the app.
""".trimIndent(),
moreInfo = "",
category = CUSTOM_LINT_CHECKS,
priority = 6,
severity = Severity.WARNING,
androidSpecific = true,
implementation = Implementation(
DaxSecureTextFieldTrailingIconDetector::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES),
),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* 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.duckduckgo.lint.ui

import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Category.Companion.CUSTOM_LINT_CHECKS
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.TextFormat
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.getParameterForArgument
import java.util.EnumSet

@Suppress("UnstableApiUsage")
class DaxTextFieldTrailingIconDetector : Detector(), SourceCodeScanner {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️


override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)

override fun createUastHandler(context: JavaContext): UElementHandler = DaxTextFieldCallHandler(context)

internal class DaxTextFieldCallHandler(private val context: JavaContext) : UElementHandler() {
override fun visitCallExpression(node: UCallExpression) {
val methodName = node.methodName

if (methodName == "DaxTextField") {
checkTrailingIconParameter(node)
}
}

private fun checkTrailingIconParameter(node: UCallExpression) {
// Find the 'trailingIcon' parameter
val trailingIconArgument = node.valueArguments.find { arg ->
val parameterName = node.getParameterForArgument(arg)?.name
parameterName == "trailingIcon"
} ?: return // No 'trailingIcon' parameter provided which is fine

// Check if the trailingIcon uses an invalid composable
if (isInvalidComposable(trailingIconArgument)) {
reportInvalidComposableUsage(trailingIconArgument)
}
}

private fun isInvalidComposable(argument: org.jetbrains.uast.UExpression): Boolean {
val source = argument.sourcePsi?.text ?: return false

// Only DaxTextFieldTrailingIcon should be used in the trailingIcon parameter
// If the source doesn't contain it, then it's invalid
return !source.contains("DaxTextFieldTrailingIcon")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably get member list declared in the scope object class and check if the source contains one member of this list? This will allow us to avoid to edit this linter rule every time we need to edit the scope.

}

private fun reportInvalidComposableUsage(arg: org.jetbrains.uast.UExpression) {
context.report(
issue = INVALID_DAX_TEXT_FIELD_TRAILING_ICON_USAGE,
location = context.getLocation(arg),
message = INVALID_DAX_TEXT_FIELD_TRAILING_ICON_USAGE.getExplanation(TextFormat.RAW),
)
}
}

companion object {
val INVALID_DAX_TEXT_FIELD_TRAILING_ICON_USAGE = Issue
.create(
id = "InvalidDaxTextFieldTrailingIconUsage",
briefDescription = "DaxTextField trailingIcon parameter should use DaxTextFieldTrailingIcon",
explanation = """
Use DaxTextFieldTrailingIcon instead of arbitrary composables for the trailingIcon parameter to maintain design system consistency.
Example:
DaxTextField(
state = state,
trailingIcon = {
DaxTextFieldTrailingIcon(
painter = painterResource(R.drawable.ic_copy_24),
contentDescription = stringResource(R.string.icon_description)
)
}
)
This ensures consistent styling, spacing, and behavior across all text field icons in the app.
""".trimIndent(),
moreInfo = "",
category = CUSTOM_LINT_CHECKS,
priority = 6,
severity = Severity.WARNING,
androidSpecific = true,
implementation = Implementation(
DaxTextFieldTrailingIconDetector::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES),
),
)
}
}
Loading
Loading