commit
6509b6ac9d
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* https://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.samples.apps.nowinandroid.core.data.repository
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.UserData
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class OfflineFirstUserDataRepository @Inject constructor(
|
||||||
|
private val niaPreferencesDataSource: NiaPreferencesDataSource
|
||||||
|
) : UserDataRepository {
|
||||||
|
|
||||||
|
override val userDataStream: Flow<UserData> =
|
||||||
|
niaPreferencesDataSource.userDataStream
|
||||||
|
|
||||||
|
override suspend fun setFollowedTopicIds(followedTopicIds: Set<String>) =
|
||||||
|
niaPreferencesDataSource.setFollowedTopicIds(followedTopicIds)
|
||||||
|
|
||||||
|
override suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) =
|
||||||
|
niaPreferencesDataSource.toggleFollowedTopicId(followedTopicId, followed)
|
||||||
|
|
||||||
|
override suspend fun setFollowedAuthorIds(followedAuthorIds: Set<String>) =
|
||||||
|
niaPreferencesDataSource.setFollowedAuthorIds(followedAuthorIds)
|
||||||
|
|
||||||
|
override suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) =
|
||||||
|
niaPreferencesDataSource.toggleFollowedAuthorId(followedAuthorId, followed)
|
||||||
|
|
||||||
|
override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) =
|
||||||
|
niaPreferencesDataSource.toggleNewsResourceBookmark(newsResourceId, bookmarked)
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* https://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.samples.apps.nowinandroid.core.data.repository
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.UserData
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface UserDataRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream of [UserData]
|
||||||
|
*/
|
||||||
|
val userDataStream: Flow<UserData>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the user's currently followed topics
|
||||||
|
*/
|
||||||
|
suspend fun setFollowedTopicIds(followedTopicIds: Set<String>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the user's newly followed/unfollowed topic
|
||||||
|
*/
|
||||||
|
suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the user's currently followed authors
|
||||||
|
*/
|
||||||
|
suspend fun setFollowedAuthorIds(followedAuthorIds: Set<String>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the user's newly followed/unfollowed author
|
||||||
|
*/
|
||||||
|
suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the bookmarked status for a news resource
|
||||||
|
*/
|
||||||
|
suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean)
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* https://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.samples.apps.nowinandroid.core.data.repository.fake
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.UserData
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fake implementation of the [AuthorsRepository] that returns hardcoded authors.
|
||||||
|
*
|
||||||
|
* This allows us to run the app with fake data, without needing an internet connection or working
|
||||||
|
* backend.
|
||||||
|
*/
|
||||||
|
class FakeUserDataRepository @Inject constructor(
|
||||||
|
private val niaPreferencesDataSource: NiaPreferencesDataSource,
|
||||||
|
) : UserDataRepository {
|
||||||
|
|
||||||
|
override val userDataStream: Flow<UserData> =
|
||||||
|
niaPreferencesDataSource.userDataStream
|
||||||
|
|
||||||
|
override suspend fun setFollowedTopicIds(followedTopicIds: Set<String>) =
|
||||||
|
niaPreferencesDataSource.setFollowedTopicIds(followedTopicIds)
|
||||||
|
|
||||||
|
override suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) =
|
||||||
|
niaPreferencesDataSource.toggleFollowedTopicId(followedTopicId, followed)
|
||||||
|
|
||||||
|
override suspend fun setFollowedAuthorIds(followedAuthorIds: Set<String>) {
|
||||||
|
niaPreferencesDataSource.setFollowedAuthorIds(followedAuthorIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) {
|
||||||
|
niaPreferencesDataSource.toggleFollowedAuthorId(followedAuthorId, followed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) {
|
||||||
|
niaPreferencesDataSource.toggleNewsResourceBookmark(newsResourceId, bookmarked)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* https://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.samples.apps.nowinandroid.core.data.repository
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
|
||||||
|
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
|
||||||
|
class OfflineFirstUserDataRepositoryTest {
|
||||||
|
private lateinit var subject: OfflineFirstUserDataRepository
|
||||||
|
|
||||||
|
private lateinit var niaPreferencesDataSource: NiaPreferencesDataSource
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
niaPreferencesDataSource = NiaPreferencesDataSource(
|
||||||
|
tmpFolder.testUserPreferencesDataStore()
|
||||||
|
)
|
||||||
|
|
||||||
|
subject = OfflineFirstUserDataRepository(
|
||||||
|
niaPreferencesDataSource = niaPreferencesDataSource
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun offlineFirstUserDataRepository_toggle_followed_topics_logic_delegates_to_nia_preferences() =
|
||||||
|
runTest {
|
||||||
|
subject.toggleFollowedTopicId(followedTopicId = "0", followed = true)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
setOf("0"),
|
||||||
|
subject.userDataStream
|
||||||
|
.map { it.followedTopics }
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
subject.toggleFollowedTopicId(followedTopicId = "1", followed = true)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
setOf("0", "1"),
|
||||||
|
subject.userDataStream
|
||||||
|
.map { it.followedTopics }
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
niaPreferencesDataSource.userDataStream
|
||||||
|
.map { it.followedTopics }
|
||||||
|
.first(),
|
||||||
|
subject.userDataStream
|
||||||
|
.map { it.followedTopics }
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun offlineFirstUserDataRepository_set_followed_topics_logic_delegates_to_nia_preferences() =
|
||||||
|
runTest {
|
||||||
|
subject.setFollowedTopicIds(followedTopicIds = setOf("1", "2"))
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
setOf("1", "2"),
|
||||||
|
subject.userDataStream
|
||||||
|
.map { it.followedTopics }
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
niaPreferencesDataSource.userDataStream
|
||||||
|
.map { it.followedTopics }
|
||||||
|
.first(),
|
||||||
|
subject.userDataStream
|
||||||
|
.map { it.followedTopics }
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun offlineFirstUserDataRepository_bookmark_news_resource_logic_delegates_to_nia_preferences() =
|
||||||
|
runTest {
|
||||||
|
subject.updateNewsResourceBookmark(newsResourceId = "0", bookmarked = true)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
setOf("0"),
|
||||||
|
subject.userDataStream
|
||||||
|
.map { it.bookmarkedNewsResources }
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
subject.updateNewsResourceBookmark(newsResourceId = "1", bookmarked = true)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
setOf("0", "1"),
|
||||||
|
subject.userDataStream
|
||||||
|
.map { it.bookmarkedNewsResources }
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
niaPreferencesDataSource.userDataStream
|
||||||
|
.map { it.bookmarkedNewsResources }
|
||||||
|
.first(),
|
||||||
|
subject.userDataStream
|
||||||
|
.map { it.bookmarkedNewsResources }
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
/build
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* https://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.
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
id("nowinandroid.android.library")
|
||||||
|
id("nowinandroid.android.library.compose")
|
||||||
|
id("nowinandroid.android.library.jacoco")
|
||||||
|
id("nowinandroid.spotless")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
lint {
|
||||||
|
checkDependencies = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
api(libs.androidx.compose.foundation)
|
||||||
|
api(libs.androidx.compose.foundation.layout)
|
||||||
|
api(libs.androidx.compose.material.iconsExtended)
|
||||||
|
api(libs.androidx.compose.material3)
|
||||||
|
debugApi(libs.androidx.compose.ui.tooling)
|
||||||
|
api(libs.androidx.compose.ui.tooling.preview)
|
||||||
|
api(libs.androidx.compose.ui.util)
|
||||||
|
api(libs.androidx.compose.runtime)
|
||||||
|
lintPublish(project(":lint"))
|
||||||
|
androidTestImplementation(project(":core-testing"))
|
||||||
|
}
|
@ -0,0 +1,276 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* https://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.samples.apps.nowinandroid.core.designsystem
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.material3.ColorScheme
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.BackgroundTheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.DarkAndroidBackgroundTheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.DarkAndroidColorScheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.DarkDefaultColorScheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LightAndroidBackgroundTheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LightAndroidColorScheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LightDefaultColorScheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LightDefaultGradientColors
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalBackgroundTheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests [NiaTheme] using different combinations of the theme mode parameters:
|
||||||
|
* darkTheme, dynamicColor, and androidTheme.
|
||||||
|
*
|
||||||
|
* It verifies that the various composition locals — [MaterialTheme], [LocalGradientColors] and
|
||||||
|
* [LocalBackgroundTheme] — have the expected values for a given theme mode, as specified by the
|
||||||
|
* design system.
|
||||||
|
*/
|
||||||
|
class ThemeTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun darkThemeFalse_dynamicColorFalse_androidThemeFalse() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
NiaTheme(
|
||||||
|
darkTheme = false,
|
||||||
|
dynamicColor = false,
|
||||||
|
androidTheme = false
|
||||||
|
) {
|
||||||
|
val colorScheme = LightDefaultColorScheme
|
||||||
|
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
|
||||||
|
val gradientColors = LightDefaultGradientColors
|
||||||
|
assertEquals(gradientColors, LocalGradientColors.current)
|
||||||
|
val backgroundTheme = BackgroundTheme(
|
||||||
|
color = colorScheme.surface,
|
||||||
|
tonalElevation = 2.dp
|
||||||
|
)
|
||||||
|
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun darkThemeTrue_dynamicColorFalse_androidThemeFalse() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
NiaTheme(
|
||||||
|
darkTheme = true,
|
||||||
|
dynamicColor = false,
|
||||||
|
androidTheme = false
|
||||||
|
) {
|
||||||
|
val colorScheme = DarkDefaultColorScheme
|
||||||
|
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
|
||||||
|
val gradientColors = GradientColors()
|
||||||
|
assertEquals(gradientColors, LocalGradientColors.current)
|
||||||
|
val backgroundTheme = BackgroundTheme(
|
||||||
|
color = colorScheme.surface,
|
||||||
|
tonalElevation = 2.dp
|
||||||
|
)
|
||||||
|
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun darkThemeFalse_dynamicColorTrue_androidThemeFalse() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
NiaTheme(
|
||||||
|
darkTheme = false,
|
||||||
|
dynamicColor = true,
|
||||||
|
androidTheme = false
|
||||||
|
) {
|
||||||
|
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
dynamicLightColorScheme(LocalContext.current)
|
||||||
|
} else {
|
||||||
|
LightDefaultColorScheme
|
||||||
|
}
|
||||||
|
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
|
||||||
|
val gradientColors = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
GradientColors()
|
||||||
|
} else {
|
||||||
|
LightDefaultGradientColors
|
||||||
|
}
|
||||||
|
assertEquals(gradientColors, LocalGradientColors.current)
|
||||||
|
val backgroundTheme = BackgroundTheme(
|
||||||
|
color = colorScheme.surface,
|
||||||
|
tonalElevation = 2.dp
|
||||||
|
)
|
||||||
|
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun darkThemeTrue_dynamicColorTrue_androidThemeFalse() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
NiaTheme(
|
||||||
|
darkTheme = true,
|
||||||
|
dynamicColor = true,
|
||||||
|
androidTheme = false
|
||||||
|
) {
|
||||||
|
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
dynamicDarkColorScheme(LocalContext.current)
|
||||||
|
} else {
|
||||||
|
DarkDefaultColorScheme
|
||||||
|
}
|
||||||
|
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
|
||||||
|
val gradientColors = GradientColors()
|
||||||
|
assertEquals(gradientColors, LocalGradientColors.current)
|
||||||
|
val backgroundTheme = BackgroundTheme(
|
||||||
|
color = colorScheme.surface,
|
||||||
|
tonalElevation = 2.dp
|
||||||
|
)
|
||||||
|
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun darkThemeFalse_dynamicColorFalse_androidThemeTrue() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
NiaTheme(
|
||||||
|
darkTheme = false,
|
||||||
|
dynamicColor = false,
|
||||||
|
androidTheme = true
|
||||||
|
) {
|
||||||
|
val colorScheme = LightAndroidColorScheme
|
||||||
|
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
|
||||||
|
val gradientColors = GradientColors()
|
||||||
|
assertEquals(gradientColors, LocalGradientColors.current)
|
||||||
|
val backgroundTheme = LightAndroidBackgroundTheme
|
||||||
|
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun darkThemeTrue_dynamicColorFalse_androidThemeTrue() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
NiaTheme(
|
||||||
|
darkTheme = true,
|
||||||
|
dynamicColor = false,
|
||||||
|
androidTheme = true
|
||||||
|
) {
|
||||||
|
val colorScheme = DarkAndroidColorScheme
|
||||||
|
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
|
||||||
|
val gradientColors = GradientColors()
|
||||||
|
assertEquals(gradientColors, LocalGradientColors.current)
|
||||||
|
val backgroundTheme = DarkAndroidBackgroundTheme
|
||||||
|
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun darkThemeFalse_dynamicColorTrue_androidThemeTrue() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
NiaTheme(
|
||||||
|
darkTheme = false,
|
||||||
|
dynamicColor = true,
|
||||||
|
androidTheme = true
|
||||||
|
) {
|
||||||
|
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
dynamicLightColorScheme(LocalContext.current)
|
||||||
|
} else {
|
||||||
|
LightDefaultColorScheme
|
||||||
|
}
|
||||||
|
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
|
||||||
|
val gradientColors = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
GradientColors()
|
||||||
|
} else {
|
||||||
|
LightDefaultGradientColors
|
||||||
|
}
|
||||||
|
assertEquals(gradientColors, LocalGradientColors.current)
|
||||||
|
val backgroundTheme = BackgroundTheme(
|
||||||
|
color = colorScheme.surface,
|
||||||
|
tonalElevation = 2.dp
|
||||||
|
)
|
||||||
|
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun darkThemeTrue_dynamicColorTrue_androidThemeTrue() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
NiaTheme(
|
||||||
|
darkTheme = true,
|
||||||
|
dynamicColor = true,
|
||||||
|
androidTheme = true
|
||||||
|
) {
|
||||||
|
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
dynamicDarkColorScheme(LocalContext.current)
|
||||||
|
} else {
|
||||||
|
DarkDefaultColorScheme
|
||||||
|
}
|
||||||
|
assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme)
|
||||||
|
val gradientColors = GradientColors()
|
||||||
|
assertEquals(gradientColors, LocalGradientColors.current)
|
||||||
|
val backgroundTheme = BackgroundTheme(
|
||||||
|
color = colorScheme.surface,
|
||||||
|
tonalElevation = 2.dp
|
||||||
|
)
|
||||||
|
assertEquals(backgroundTheme, LocalBackgroundTheme.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workaround for the fact that the NiA design system specify all color scheme values.
|
||||||
|
*/
|
||||||
|
private fun assertColorSchemesEqual(
|
||||||
|
expectedColorScheme: ColorScheme,
|
||||||
|
actualColorScheme: ColorScheme
|
||||||
|
) {
|
||||||
|
assertEquals(expectedColorScheme.primary, actualColorScheme.primary)
|
||||||
|
assertEquals(expectedColorScheme.onPrimary, actualColorScheme.onPrimary)
|
||||||
|
assertEquals(expectedColorScheme.primaryContainer, actualColorScheme.primaryContainer)
|
||||||
|
assertEquals(expectedColorScheme.onPrimaryContainer, actualColorScheme.onPrimaryContainer)
|
||||||
|
assertEquals(expectedColorScheme.secondary, actualColorScheme.secondary)
|
||||||
|
assertEquals(expectedColorScheme.onSecondary, actualColorScheme.onSecondary)
|
||||||
|
assertEquals(expectedColorScheme.secondaryContainer, actualColorScheme.secondaryContainer)
|
||||||
|
assertEquals(
|
||||||
|
expectedColorScheme.onSecondaryContainer,
|
||||||
|
actualColorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
assertEquals(expectedColorScheme.tertiary, actualColorScheme.tertiary)
|
||||||
|
assertEquals(expectedColorScheme.onTertiary, actualColorScheme.onTertiary)
|
||||||
|
assertEquals(expectedColorScheme.tertiaryContainer, actualColorScheme.tertiaryContainer)
|
||||||
|
assertEquals(expectedColorScheme.onTertiaryContainer, actualColorScheme.onTertiaryContainer)
|
||||||
|
assertEquals(expectedColorScheme.error, actualColorScheme.error)
|
||||||
|
assertEquals(expectedColorScheme.onError, actualColorScheme.onError)
|
||||||
|
assertEquals(expectedColorScheme.errorContainer, actualColorScheme.errorContainer)
|
||||||
|
assertEquals(expectedColorScheme.onErrorContainer, actualColorScheme.onErrorContainer)
|
||||||
|
assertEquals(expectedColorScheme.background, actualColorScheme.background)
|
||||||
|
assertEquals(expectedColorScheme.onBackground, actualColorScheme.onBackground)
|
||||||
|
assertEquals(expectedColorScheme.surface, actualColorScheme.surface)
|
||||||
|
assertEquals(expectedColorScheme.onSurface, actualColorScheme.onSurface)
|
||||||
|
assertEquals(expectedColorScheme.surfaceVariant, actualColorScheme.surfaceVariant)
|
||||||
|
assertEquals(expectedColorScheme.onSurfaceVariant, actualColorScheme.onSurfaceVariant)
|
||||||
|
assertEquals(expectedColorScheme.outline, actualColorScheme.outline)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* https://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.samples.apps.nowinandroid.core.designsystem.component
|
||||||
|
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ProvideTextStyle
|
||||||
|
import androidx.compose.material3.Shapes
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now in Android filter chip with included leading checked icon as well as text content slot.
|
||||||
|
*
|
||||||
|
* @param selected Whether the chip is currently checked.
|
||||||
|
* @param onSelectedChange Called when the user clicks the chip and toggles checked.
|
||||||
|
* @param modifier Modifier to be applied to the chip.
|
||||||
|
* @param enabled Controls the enabled state of the chip. When `false`, this chip will not be
|
||||||
|
* clickable and will appear disabled to accessibility services.
|
||||||
|
* @param label The text label content.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun NiaFilterChip(
|
||||||
|
selected: Boolean,
|
||||||
|
onSelectedChange: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
label: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
selected = selected,
|
||||||
|
onClick = { onSelectedChange(!selected) },
|
||||||
|
label = {
|
||||||
|
ProvideTextStyle(value = MaterialTheme.typography.labelSmall) {
|
||||||
|
label()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
selectedIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = NiaIcons.Check,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
shape = Shapes.Full,
|
||||||
|
border = FilterChipDefaults.filterChipBorder(
|
||||||
|
borderColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
selectedBorderColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
disabledBorderColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||||
|
alpha = NiaChipDefaults.DisabledChipContentAlpha
|
||||||
|
),
|
||||||
|
disabledSelectedBorderColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||||
|
alpha = NiaChipDefaults.DisabledChipContentAlpha
|
||||||
|
),
|
||||||
|
borderWidth = NiaChipDefaults.ChipBorderWidth,
|
||||||
|
selectedBorderWidth = NiaChipDefaults.ChipBorderWidth
|
||||||
|
),
|
||||||
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
labelColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
iconColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
disabledContainerColor = if (selected) {
|
||||||
|
MaterialTheme.colorScheme.onBackground.copy(
|
||||||
|
alpha = NiaChipDefaults.DisabledChipContainerAlpha
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Color.Transparent
|
||||||
|
},
|
||||||
|
disabledLabelColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||||
|
alpha = NiaChipDefaults.DisabledChipContentAlpha
|
||||||
|
),
|
||||||
|
disabledLeadingIconColor = MaterialTheme.colorScheme.onBackground.copy(
|
||||||
|
alpha = NiaChipDefaults.DisabledChipContentAlpha
|
||||||
|
),
|
||||||
|
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
selectedLabelColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
selectedLeadingIconColor = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now in Android chip default values.
|
||||||
|
*/
|
||||||
|
object NiaChipDefaults {
|
||||||
|
const val DisabledChipContainerAlpha = 0.12f
|
||||||
|
const val DisabledChipContentAlpha = 0.38f
|
||||||
|
val ChipBorderWidth = 1.dp
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* https://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.samples.apps.nowinandroid.core.designsystem.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now in Android colors.
|
||||||
|
*/
|
||||||
|
internal val Blue10 = Color(0xFF001F29)
|
||||||
|
internal val Blue20 = Color(0xFF003544)
|
||||||
|
internal val Blue30 = Color(0xFF004D61)
|
||||||
|
internal val Blue40 = Color(0xFF006781)
|
||||||
|
internal val Blue80 = Color(0xFF5DD4FB)
|
||||||
|
internal val Blue90 = Color(0xFFB5EAFF)
|
||||||
|
internal val Blue95 = Color(0xFFDCF5FF)
|
||||||
|
internal val DarkGreen10 = Color(0xFF0D1F12)
|
||||||
|
internal val DarkGreen20 = Color(0xFF223526)
|
||||||
|
internal val DarkGreen30 = Color(0xFF394B3C)
|
||||||
|
internal val DarkGreen40 = Color(0xFF4F6352)
|
||||||
|
internal val DarkGreen80 = Color(0xFFB7CCB8)
|
||||||
|
internal val DarkGreen90 = Color(0xFFD3E8D3)
|
||||||
|
internal val DarkGreenGray10 = Color(0xFF1A1C1A)
|
||||||
|
internal val DarkGreenGray90 = Color(0xFFE2E3DE)
|
||||||
|
internal val DarkGreenGray95 = Color(0xFFF0F1EC)
|
||||||
|
internal val DarkGreenGray99 = Color(0xFFFBFDF7)
|
||||||
|
internal val DarkPurpleGray10 = Color(0xFF201A1B)
|
||||||
|
internal val DarkPurpleGray90 = Color(0xFFECDFE0)
|
||||||
|
internal val DarkPurpleGray95 = Color(0xFFFAEEEF)
|
||||||
|
internal val DarkPurpleGray99 = Color(0xFFFCFCFC)
|
||||||
|
internal val Green10 = Color(0xFF00210B)
|
||||||
|
internal val Green20 = Color(0xFF003919)
|
||||||
|
internal val Green30 = Color(0xFF005227)
|
||||||
|
internal val Green40 = Color(0xFF006D36)
|
||||||
|
internal val Green80 = Color(0xFF0EE37C)
|
||||||
|
internal val Green90 = Color(0xFF5AFF9D)
|
||||||
|
internal val GreenGray30 = Color(0xFF414941)
|
||||||
|
internal val GreenGray50 = Color(0xFF727971)
|
||||||
|
internal val GreenGray60 = Color(0xFF8B938A)
|
||||||
|
internal val GreenGray80 = Color(0xFFC1C9BF)
|
||||||
|
internal val GreenGray90 = Color(0xFFDDE5DB)
|
||||||
|
internal val Orange10 = Color(0xFF390C00)
|
||||||
|
internal val Orange20 = Color(0xFF5D1900)
|
||||||
|
internal val Orange30 = Color(0xFF812800)
|
||||||
|
internal val Orange40 = Color(0xFFA23F16)
|
||||||
|
internal val Orange80 = Color(0xFFFFB599)
|
||||||
|
internal val Orange90 = Color(0xFFFFDBCE)
|
||||||
|
internal val Orange95 = Color(0xFFFFEDE6)
|
||||||
|
internal val Purple10 = Color(0xFF36003D)
|
||||||
|
internal val Purple20 = Color(0xFF560A5E)
|
||||||
|
internal val Purple30 = Color(0xFF702776)
|
||||||
|
internal val Purple40 = Color(0xFF8C4190)
|
||||||
|
internal val Purple80 = Color(0xFFFFA8FF)
|
||||||
|
internal val Purple90 = Color(0xFFFFD5FC)
|
||||||
|
internal val Purple95 = Color(0xFFFFEBFB)
|
||||||
|
internal val PurpleGray30 = Color(0xFF4E444C)
|
||||||
|
internal val PurpleGray50 = Color(0xFF7F747C)
|
||||||
|
internal val PurpleGray60 = Color(0xFF998D96)
|
||||||
|
internal val PurpleGray80 = Color(0xFFD0C2CC)
|
||||||
|
internal val PurpleGray90 = Color(0xFFEDDEE8)
|
||||||
|
internal val Red10 = Color(0xFF410001)
|
||||||
|
internal val Red20 = Color(0xFF680003)
|
||||||
|
internal val Red30 = Color(0xFF930006)
|
||||||
|
internal val Red40 = Color(0xFFBA1B1B)
|
||||||
|
internal val Red80 = Color(0xFFFFB4A9)
|
||||||
|
internal val Red90 = Color(0xFFFFDAD4)
|
||||||
|
internal val Teal10 = Color(0xFF001F26)
|
||||||
|
internal val Teal20 = Color(0xFF02363F)
|
||||||
|
internal val Teal30 = Color(0xFF214D56)
|
||||||
|
internal val Teal40 = Color(0xFF3A656F)
|
||||||
|
internal val Teal80 = Color(0xFFA2CED9)
|
||||||
|
internal val Teal90 = Color(0xFFBEEAF6)
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* https://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.samples.apps.nowinandroid.core.designsystem.theme
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to model gradient color values for Now in Android.
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
data class GradientColors(
|
||||||
|
val primary: Color = Color.Unspecified,
|
||||||
|
val secondary: Color = Color.Unspecified,
|
||||||
|
val tertiary: Color = Color.Unspecified,
|
||||||
|
val neutral: Color = Color.Unspecified
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A composition local for [GradientColors].
|
||||||
|
*/
|
||||||
|
val LocalGradientColors = staticCompositionLocalOf { GradientColors() }
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* https://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.samples.apps.nowinandroid.core.model.data
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class summarizing user interest data
|
||||||
|
*/
|
||||||
|
data class UserData(
|
||||||
|
val bookmarkedNewsResources: Set<String>,
|
||||||
|
val followedTopics: Set<String>,
|
||||||
|
val followedAuthors: Set<String>,
|
||||||
|
)
|
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* https://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.samples.apps.nowinandroid.core.testing.repository
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.UserData
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
|
||||||
|
private val emptyUserData = UserData(
|
||||||
|
bookmarkedNewsResources = emptySet(),
|
||||||
|
followedTopics = emptySet(),
|
||||||
|
followedAuthors = emptySet()
|
||||||
|
)
|
||||||
|
|
||||||
|
class TestUserDataRepository : UserDataRepository {
|
||||||
|
/**
|
||||||
|
* The backing hot flow for the list of followed topic ids for testing.
|
||||||
|
*/
|
||||||
|
private val _userData = MutableSharedFlow<UserData>(replay = 1, onBufferOverflow = DROP_OLDEST)
|
||||||
|
|
||||||
|
private val currentUserData get() = _userData.replayCache.firstOrNull() ?: emptyUserData
|
||||||
|
|
||||||
|
override val userDataStream: Flow<UserData> = _userData.filterNotNull()
|
||||||
|
|
||||||
|
override suspend fun setFollowedTopicIds(followedTopicIds: Set<String>) {
|
||||||
|
_userData.tryEmit(currentUserData.copy(followedTopics = followedTopicIds))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) {
|
||||||
|
currentUserData.let { current ->
|
||||||
|
val followedTopics = if (followed) current.followedTopics + followedTopicId
|
||||||
|
else current.followedTopics - followedTopicId
|
||||||
|
|
||||||
|
_userData.tryEmit(current.copy(followedTopics = followedTopics))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setFollowedAuthorIds(followedAuthorIds: Set<String>) {
|
||||||
|
_userData.tryEmit(currentUserData.copy(followedAuthors = followedAuthorIds))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) {
|
||||||
|
currentUserData.let { current ->
|
||||||
|
val followedAuthors = if (followed) current.followedAuthors + followedAuthorId
|
||||||
|
else current.followedAuthors - followedAuthorId
|
||||||
|
|
||||||
|
_userData.tryEmit(current.copy(followedAuthors = followedAuthors))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) {
|
||||||
|
currentUserData.let { current ->
|
||||||
|
val bookmarkedNews = if (bookmarked) current.bookmarkedNewsResources + newsResourceId
|
||||||
|
else current.bookmarkedNewsResources - newsResourceId
|
||||||
|
|
||||||
|
_userData.tryEmit(current.copy(bookmarkedNewsResources = bookmarkedNews))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A test-only API to allow querying the current followed topics.
|
||||||
|
*/
|
||||||
|
fun getCurrentFollowedTopics(): Set<String>? =
|
||||||
|
_userData.replayCache.firstOrNull()?.followedTopics
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A test-only API to allow querying the current followed authors.
|
||||||
|
*/
|
||||||
|
fun getCurrentFollowedAuthors(): Set<String>? =
|
||||||
|
_userData.replayCache.firstOrNull()?.followedAuthors
|
||||||
|
}
|
@ -1,106 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 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
|
|
||||||
*
|
|
||||||
* https://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.samples.apps.nowinandroid.core.ui
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.selection.toggleable
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.icons.Icons.Filled
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.Done
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.composed
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun FollowButton(
|
|
||||||
following: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
onFollowChange: ((Boolean) -> Unit)? = null,
|
|
||||||
backgroundColor: Color = Color.Transparent,
|
|
||||||
size: Dp = 32.dp,
|
|
||||||
iconSize: Dp = size / 2,
|
|
||||||
followingContentDescription: String? = null,
|
|
||||||
notFollowingContentDescription: String? = null,
|
|
||||||
) {
|
|
||||||
val background = if (following) {
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
} else {
|
|
||||||
backgroundColor
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = modifier.followButton(onFollowChange, following, enabled, background, size),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
if (following) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Filled.Done,
|
|
||||||
contentDescription = followingContentDescription,
|
|
||||||
modifier = Modifier.size(iconSize)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Icon(
|
|
||||||
imageVector = Filled.Add,
|
|
||||||
contentDescription = notFollowingContentDescription,
|
|
||||||
modifier = Modifier.size(iconSize)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Modifier.followButton(
|
|
||||||
onFollowChange: ((Boolean) -> Unit)?,
|
|
||||||
following: Boolean,
|
|
||||||
enabled: Boolean,
|
|
||||||
background: Color,
|
|
||||||
size: Dp
|
|
||||||
): Modifier = composed {
|
|
||||||
val boxModifier = if (onFollowChange != null) {
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
val ripple = rememberRipple(bounded = false, radius = 24.dp)
|
|
||||||
this
|
|
||||||
.toggleable(
|
|
||||||
value = following,
|
|
||||||
onValueChange = onFollowChange,
|
|
||||||
enabled = enabled,
|
|
||||||
role = Role.Checkbox,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = ripple
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
boxModifier
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(background)
|
|
||||||
.size(size)
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 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
|
|
||||||
*
|
|
||||||
* https://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.samples.apps.nowinandroid.core.ui.component
|
|
||||||
|
|
||||||
import androidx.compose.foundation.selection.toggleable
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Now in Android filter chip with included leading checked icon as well as text content slot.
|
|
||||||
*
|
|
||||||
* @param checked Whether the chip is currently checked.
|
|
||||||
* @param onCheckedChange Called when the user clicks the chip and toggles checked.
|
|
||||||
* @param modifier Modifier to be applied to the chip.
|
|
||||||
* @param enabled Controls the enabled state of the chip. When `false`, this chip will not be
|
|
||||||
* clickable and will appear disabled to accessibility services.
|
|
||||||
* @param text The text label content.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun NiaFilterChip(
|
|
||||||
checked: Boolean,
|
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
text: @Composable () -> Unit
|
|
||||||
) {
|
|
||||||
// TODO: Replace with Chip when available in Compose Material 3: b/197399111
|
|
||||||
NiaOutlinedButton(
|
|
||||||
onClick = { onCheckedChange(!checked) },
|
|
||||||
modifier = Modifier
|
|
||||||
.toggleable(value = checked, enabled = enabled, role = Role.Button, onValueChange = {})
|
|
||||||
.then(modifier),
|
|
||||||
enabled = enabled,
|
|
||||||
small = true,
|
|
||||||
border = NiaButtonDefaults.outlinedButtonBorder(
|
|
||||||
enabled = enabled,
|
|
||||||
disabledColor = MaterialTheme.colorScheme.onBackground.copy(
|
|
||||||
alpha = if (checked) {
|
|
||||||
NiaButtonDefaults.DisabledButtonContentAlpha
|
|
||||||
} else {
|
|
||||||
NiaButtonDefaults.DisabledButtonContainerAlpha
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
colors = NiaButtonDefaults.outlinedButtonColors(
|
|
||||||
containerColor = if (checked) {
|
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
|
||||||
} else {
|
|
||||||
Color.Transparent
|
|
||||||
},
|
|
||||||
disabledContainerColor = if (checked) {
|
|
||||||
MaterialTheme.colorScheme.onBackground.copy(
|
|
||||||
alpha = NiaButtonDefaults.DisabledButtonContainerAlpha
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Color.Transparent
|
|
||||||
}
|
|
||||||
),
|
|
||||||
text = text,
|
|
||||||
leadingIcon = if (checked) {
|
|
||||||
{
|
|
||||||
Icon(
|
|
||||||
imageVector = NiaIcons.Check,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,108 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 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
|
|
||||||
*
|
|
||||||
* https://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.samples.apps.nowinandroid.core.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.core.graphics.ColorUtils
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Now in Android colors.
|
|
||||||
*/
|
|
||||||
val Blue10 = Color(0xFF001F29)
|
|
||||||
val Blue20 = Color(0xFF003544)
|
|
||||||
val Blue30 = Color(0xFF004D61)
|
|
||||||
val Blue40 = Color(0xFF006781)
|
|
||||||
val Blue80 = Color(0xFF5DD4FB)
|
|
||||||
val Blue90 = Color(0xFFB5EAFF)
|
|
||||||
val Blue95 = Color(0xFFDCF5FF)
|
|
||||||
val DarkGreen10 = Color(0xFF0D1F12)
|
|
||||||
val DarkGreen20 = Color(0xFF223526)
|
|
||||||
val DarkGreen30 = Color(0xFF394B3C)
|
|
||||||
val DarkGreen40 = Color(0xFF4F6352)
|
|
||||||
val DarkGreen80 = Color(0xFFB7CCB8)
|
|
||||||
val DarkGreen90 = Color(0xFFD3E8D3)
|
|
||||||
val DarkGreenGray10 = Color(0xFF1A1C1A)
|
|
||||||
val DarkGreenGray90 = Color(0xFFE2E3DE)
|
|
||||||
val DarkGreenGray95 = Color(0xFFF0F1EC)
|
|
||||||
val DarkGreenGray99 = Color(0xFFFBFDF7)
|
|
||||||
val DarkPurpleGray10 = Color(0xFF201A1B)
|
|
||||||
val DarkPurpleGray90 = Color(0xFFECDFE0)
|
|
||||||
val DarkPurpleGray95 = Color(0xFFFAEEEF)
|
|
||||||
val DarkPurpleGray99 = Color(0xFFFCFCFC)
|
|
||||||
val Green10 = Color(0xFF00210B)
|
|
||||||
val Green20 = Color(0xFF003919)
|
|
||||||
val Green30 = Color(0xFF005227)
|
|
||||||
val Green40 = Color(0xFF006D36)
|
|
||||||
val Green80 = Color(0xFF0EE37C)
|
|
||||||
val Green90 = Color(0xFF5AFF9D)
|
|
||||||
val GreenGray30 = Color(0xFF414941)
|
|
||||||
val GreenGray50 = Color(0xFF727971)
|
|
||||||
val GreenGray60 = Color(0xFF8B938A)
|
|
||||||
val GreenGray80 = Color(0xFFC1C9BF)
|
|
||||||
val GreenGray90 = Color(0xFFDDE5DB)
|
|
||||||
val Orange10 = Color(0xFF390C00)
|
|
||||||
val Orange20 = Color(0xFF5D1900)
|
|
||||||
val Orange30 = Color(0xFF812800)
|
|
||||||
val Orange40 = Color(0xFFA23F16)
|
|
||||||
val Orange80 = Color(0xFFFFB599)
|
|
||||||
val Orange90 = Color(0xFFFFDBCE)
|
|
||||||
val Orange95 = Color(0xFFFFEDE6)
|
|
||||||
val Purple10 = Color(0xFF36003D)
|
|
||||||
val Purple20 = Color(0xFF560A5E)
|
|
||||||
val Purple30 = Color(0xFF702776)
|
|
||||||
val Purple40 = Color(0xFF8C4190)
|
|
||||||
val Purple80 = Color(0xFFFFA8FF)
|
|
||||||
val Purple90 = Color(0xFFFFD5FC)
|
|
||||||
val Purple95 = Color(0xFFFFEBFB)
|
|
||||||
val PurpleGray30 = Color(0xFF4E444C)
|
|
||||||
val PurpleGray50 = Color(0xFF7F747C)
|
|
||||||
val PurpleGray60 = Color(0xFF998D96)
|
|
||||||
val PurpleGray80 = Color(0xFFD0C2CC)
|
|
||||||
val PurpleGray90 = Color(0xFFEDDEE8)
|
|
||||||
val Red10 = Color(0xFF410001)
|
|
||||||
val Red20 = Color(0xFF680003)
|
|
||||||
val Red30 = Color(0xFF930006)
|
|
||||||
val Red40 = Color(0xFFBA1B1B)
|
|
||||||
val Red80 = Color(0xFFFFB4A9)
|
|
||||||
val Red90 = Color(0xFFFFDAD4)
|
|
||||||
val Teal10 = Color(0xFF001F26)
|
|
||||||
val Teal20 = Color(0xFF02363F)
|
|
||||||
val Teal30 = Color(0xFF214D56)
|
|
||||||
val Teal40 = Color(0xFF3A656F)
|
|
||||||
val Teal80 = Color(0xFFA2CED9)
|
|
||||||
val Teal90 = Color(0xFFBEEAF6)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lighten the current [Color] instance to the given [luminance].
|
|
||||||
*
|
|
||||||
* This is needed because we can't access the token values directly. For the dynamic color theme,
|
|
||||||
* this makes it impossible to get the 95% luminance token of the different theme colors.
|
|
||||||
* TODO: Link to bug
|
|
||||||
*/
|
|
||||||
internal fun Color.lighten(luminance: Float): Color {
|
|
||||||
val hsl = FloatArray(3)
|
|
||||||
ColorUtils.RGBToHSL(
|
|
||||||
(red * 256).roundToInt(),
|
|
||||||
(green * 256).roundToInt(),
|
|
||||||
(blue * 256).roundToInt(),
|
|
||||||
hsl
|
|
||||||
)
|
|
||||||
hsl[2] = luminance
|
|
||||||
val color = Color(ColorUtils.HSLToColor(hsl))
|
|
||||||
return color
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
|||||||
|
/build
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* https://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.
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
id 'java-library'
|
||||||
|
id 'kotlin'
|
||||||
|
id 'com.android.lint'
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly libs.kotlin.stdlib
|
||||||
|
compileOnly libs.lint.api
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* https://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.samples.apps.nowinandroid.lint.designsystem
|
||||||
|
|
||||||
|
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 org.jetbrains.uast.UCallExpression
|
||||||
|
import org.jetbrains.uast.UElement
|
||||||
|
import org.jetbrains.uast.UQualifiedReferenceExpression
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A detector that checks for incorrect usages of Compose Material APIs over equivalents in
|
||||||
|
* the Now in Android design system module.
|
||||||
|
*/
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
class DesignSystemDetector : Detector(), Detector.UastScanner {
|
||||||
|
|
||||||
|
override fun getApplicableUastTypes(): List<Class<out UElement>> {
|
||||||
|
return listOf(
|
||||||
|
UCallExpression::class.java,
|
||||||
|
UQualifiedReferenceExpression::class.java
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createUastHandler(context: JavaContext): UElementHandler {
|
||||||
|
return object : UElementHandler() {
|
||||||
|
override fun visitCallExpression(node: UCallExpression) {
|
||||||
|
val name = node.methodName ?: return
|
||||||
|
val preferredName = METHOD_NAMES[name] ?: return
|
||||||
|
reportIssue(context, node, name, preferredName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) {
|
||||||
|
val name = node.receiver.asRenderString()
|
||||||
|
val preferredName = RECEIVER_NAMES[name] ?: return
|
||||||
|
reportIssue(context, node, name, preferredName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmField
|
||||||
|
val ISSUE: Issue = Issue.create(
|
||||||
|
id = "DesignSystem",
|
||||||
|
briefDescription = "Design system",
|
||||||
|
explanation = "This check highlights calls in code that use Compose Material " +
|
||||||
|
"composables instead of equivalents from the Now in Android design system " +
|
||||||
|
"module.",
|
||||||
|
category = Category.CUSTOM_LINT_CHECKS,
|
||||||
|
priority = 7,
|
||||||
|
severity = Severity.ERROR,
|
||||||
|
implementation = Implementation(
|
||||||
|
DesignSystemDetector::class.java,
|
||||||
|
Scope.JAVA_FILE_SCOPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unfortunately :lint is a Java module and thus can't depend on the :core-designsystem
|
||||||
|
// Android module, so we can't use composable function references (eg. ::Button.name)
|
||||||
|
// instead of hardcoded names.
|
||||||
|
val METHOD_NAMES = mapOf(
|
||||||
|
"MaterialTheme" to "NiaTheme",
|
||||||
|
"Button" to "NiaFilledButton",
|
||||||
|
"OutlinedButton" to "NiaOutlinedButton",
|
||||||
|
"TextButton" to "NiaTextButton",
|
||||||
|
"FilterChip" to "NiaFilterChip",
|
||||||
|
"ElevatedFilterChip" to "NiaFilterChip",
|
||||||
|
"DropdownMenu" to "NiaDropdownMenu",
|
||||||
|
"NavigationBar" to "NiaNavigationBar",
|
||||||
|
"NavigationBarItem" to "NiaNavigationBarItem",
|
||||||
|
"NavigationRail" to "NiaNavigationRail",
|
||||||
|
"NavigationRailItem" to "NiaNavigationRailItem",
|
||||||
|
"TabRow" to "NiaTabRow",
|
||||||
|
"Tab" to "NiaTab",
|
||||||
|
"IconToggleButton" to "NiaToggleButton",
|
||||||
|
"FilledIconToggleButton" to "NiaToggleButton",
|
||||||
|
"FilledTonalIconToggleButton" to "NiaToggleButton",
|
||||||
|
"OutlinedIconToggleButton" to "NiaToggleButton",
|
||||||
|
"CenterAlignedTopAppBar" to "NiaTopAppBar",
|
||||||
|
"SmallTopAppBar" to "NiaTopAppBar",
|
||||||
|
"MediumTopAppBar" to "NiaTopAppBar",
|
||||||
|
"LargeTopAppBar" to "NiaTopAppBar"
|
||||||
|
)
|
||||||
|
val RECEIVER_NAMES = mapOf(
|
||||||
|
"Icons" to "NiaIcons"
|
||||||
|
)
|
||||||
|
|
||||||
|
fun reportIssue(
|
||||||
|
context: JavaContext,
|
||||||
|
node: UElement,
|
||||||
|
name: String,
|
||||||
|
preferredName: String
|
||||||
|
) {
|
||||||
|
context.report(
|
||||||
|
ISSUE, node, context.getLocation(node),
|
||||||
|
"Using $name instead of $preferredName"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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
|
||||||
|
*
|
||||||
|
* https://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.samples.apps.nowinandroid.lint.designsystem
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An issue registry that checks for incorrect usages of Compose Material APIs over equivalents in
|
||||||
|
* the Now in Android design system module.
|
||||||
|
*/
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
class DesignSystemIssueRegistry : IssueRegistry() {
|
||||||
|
override val issues = listOf(DesignSystemDetector.ISSUE)
|
||||||
|
|
||||||
|
override val api: Int = CURRENT_API
|
||||||
|
|
||||||
|
override val minApi: Int = 12
|
||||||
|
|
||||||
|
override val vendor: Vendor = Vendor(
|
||||||
|
vendorName = "Now in Android",
|
||||||
|
feedbackUrl = "https://github.com/android/nowinandroid/issues",
|
||||||
|
contact = "https://github.com/android/nowinandroid"
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
#
|
||||||
|
# Copyright 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
com.google.samples.apps.nowinandroid.lint.designsystem.DesignSystemIssueRegistry
|
Loading…
Reference in new issue