blob: 15dd7b92465ee67552385b7f259f8517a87b9b65 [file] [log] [blame]
/*
* 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.
*/
@file:JvmName("ThemeUtils")
package com.android.tools.idea.configurations
import com.android.SdkConstants
import com.android.SdkConstants.PREFIX_ANDROID
import com.android.SdkConstants.STYLE_RESOURCE_PREFIX
import com.android.ide.common.rendering.HardwareConfigHelper
import com.android.ide.common.rendering.api.StyleResourceValue
import com.android.ide.common.resources.ResourceResolver.THEME_NAME
import com.android.resources.ScreenSize
import com.android.sdklib.IAndroidTarget
import com.android.sdklib.devices.Device
import com.android.tools.idea.editors.theme.ThemeResolver
import com.android.tools.idea.editors.theme.datamodels.ConfiguredThemeEditorStyle
import com.android.tools.idea.model.ActivityAttributesSnapshot
import com.android.tools.idea.model.AndroidManifestIndex
import com.android.tools.idea.model.AndroidModuleInfo
import com.android.tools.idea.model.MergedManifestManager
import com.android.tools.idea.model.logManifestIndexQueryError
import com.android.tools.idea.model.queryActivitiesFromManifestIndex
import com.android.tools.idea.model.queryApplicationThemeFromManifestIndex
import com.android.tools.idea.run.activity.DefaultActivityLocator
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.IndexNotReadyException
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Computable
import org.jetbrains.android.facet.AndroidFacet
private const val ANDROID_THEME = PREFIX_ANDROID + "Theme"
private const val ANDROID_THEME_PREFIX = PREFIX_ANDROID + "Theme."
private const val PROJECT_THEME_PREFIX = "Theme."
private const val PROJECT_THEME = "Theme"
private const val RECENTLY_USED_THEMES_PROPERTY = "android.recentlyUsedThemes"
private const val MAX_RECENTLY_USED_THEMES = 5
typealias ThemeStyleFilter = (ConfiguredThemeEditorStyle) -> Boolean
/**
* If the [theme] is called "Theme" or "android:Theme", returns "Theme".
* Otherwise, if the [theme] has prefix "android:Theme.", "Theme.", or "@style/", removes it.
*/
internal fun getPreferredThemeName(theme: String): String {
if (theme == ANDROID_THEME || theme == PROJECT_THEME) {
return THEME_NAME
}
return theme.removePrefix(ANDROID_THEME_PREFIX).removePrefix(PROJECT_THEME_PREFIX).removePrefix(STYLE_RESOURCE_PREFIX)
}
fun createFilter(resolver: ThemeResolver, excludedNames: Set<String>, vararg baseThemes: StyleResourceValue): ThemeStyleFilter {
if (baseThemes.isEmpty()) {
return { style: ConfiguredThemeEditorStyle -> !excludedNames.contains(style.qualifiedName)}
}
else {
return { style: ConfiguredThemeEditorStyle -> !excludedNames.contains(style.qualifiedName) &&
resolver.themeIsChildOfAny(style.styleResourceValue, *baseThemes)}
}
}
fun getFrameworkThemes(themeResolver: ThemeResolver): List<ConfiguredThemeEditorStyle> =
getFilteredByPrefixSortedByName(getPublicThemes(themeResolver.frameworkThemes))
fun getFrameworkThemeNames(themeResolver: ThemeResolver, filter: ThemeStyleFilter) =
getFilteredNames(getFrameworkThemes(themeResolver), filter)
fun getProjectThemes(themeResolver: ThemeResolver): List<ConfiguredThemeEditorStyle> =
getFilteredByPrefixSortedByName(getPublicThemes(themeResolver.localThemes))
fun getProjectThemeNames(themeResolver: ThemeResolver, filter: ThemeStyleFilter) =
getFilteredNames(getProjectThemes(themeResolver), filter)
fun getLibraryThemes(themeResolver: ThemeResolver): List<ConfiguredThemeEditorStyle> =
getFilteredByPrefixSortedByName(getPublicThemes(themeResolver.externalLibraryThemes), setOf("Base.", "Platform."))
fun getLibraryThemeNames(themeResolver: ThemeResolver, filter: ThemeStyleFilter) =
getFilteredNames(getPublicThemes(themeResolver.externalLibraryThemes), filter)
fun getRecommendedThemes(themeResolver: ThemeResolver): List<ConfiguredThemeEditorStyle> {
val recommendedThemes = themeResolver.recommendedThemes
return sequenceOf(getLibraryThemes(themeResolver), getFrameworkThemes(themeResolver))
.flatten()
.filter { it.styleReference in recommendedThemes }
.toList()
}
fun getRecommendedThemeNames(themeResolver: ThemeResolver, filter: ThemeStyleFilter) =
getFilteredNames(getRecommendedThemes(themeResolver), filter)
// TODO: Handle namespace issues around recently used themes
@JvmOverloads
fun getRecentlyUsedThemes(project: Project, excludedNames: Set<String> = emptySet()) =
PropertiesComponent.getInstance(project)
.getValues(RECENTLY_USED_THEMES_PROPERTY)
?.asList()
?.minus(excludedNames) ?: emptyList()
fun addRecentlyUsedTheme(project: Project, theme: String) {
// The recently used themes are not shared between different projects.
val old = PropertiesComponent.getInstance(project).getValues(RECENTLY_USED_THEMES_PROPERTY)?.toSet() ?: emptySet()
val new = setOf(theme).plus(old).take(MAX_RECENTLY_USED_THEMES).toTypedArray()
PropertiesComponent.getInstance(project).setValues(RECENTLY_USED_THEMES_PROPERTY, new)
}
/**
* Filters a collection of themes to return a new collection with only the public ones.
*/
private fun getPublicThemes(themes: List<ConfiguredThemeEditorStyle>) = themes.filter { it.isPublic }
/**
* Returns the [themes] excluding those with names starting with prefixes in [excludedPrefixes] sorted by qualified name.
*/
private fun getFilteredByPrefixSortedByName(themes: List<ConfiguredThemeEditorStyle>,
excludedPrefixes: Set<String> = emptySet()): List<ConfiguredThemeEditorStyle> =
themes
.filter { theme -> excludedPrefixes.none({ prefix -> theme.name.startsWith(prefix) })}
.sortedBy { it.qualifiedName }
/**
* Returns the names of the [themes] excluding those filtered out by the specified [filter].
*/
private fun getFilteredNames(themes: List<ConfiguredThemeEditorStyle>, filter: ThemeStyleFilter) =
themes
.filter(filter)
.map { it.qualifiedName }
/**
* Try to get application theme from [AndroidManifestIndex] if index is enabled. And it falls back to the merged
* manifest snapshot if necessary.
*/
fun Module.getAppThemeName(): String? {
if (AndroidManifestIndex.indexEnabled()) {
try {
val facet = AndroidFacet.getInstance(this)
if (facet != null) {
return DumbService.getInstance(this.project).runReadActionInSmartMode(Computable {
facet.queryApplicationThemeFromManifestIndex()
})
}
}
catch (e: IndexNotReadyException) {
// TODO(147116755): runReadActionInSmartMode doesn't work if we already have read access.
// We need to refactor the callers of this to require a *smart*
// read action, at which point we can remove this try-catch.
logManifestIndexQueryError(e);
}
}
return MergedManifestManager.getFreshSnapshot(this).manifestTheme
}
/**
* Try to get activity themes from [AndroidManifestIndex] if index is enabled. And it falls back to the merged
* manifest snapshot if necessary.
*/
fun Module.getAllActivityThemeNames(): Set<String> {
if (AndroidManifestIndex.indexEnabled()) {
try {
val facet = AndroidFacet.getInstance(this)
if (facet != null) {
return DumbService.getInstance(this.project).runReadActionInSmartMode(Computable {
val activities = facet.queryActivitiesFromManifestIndex().activities
activities.asSequence()
.mapNotNull(DefaultActivityLocator.ActivityWrapper::getTheme)
.toSet()
})
}
}
catch (e: IndexNotReadyException) {
// TODO(147116755): runReadActionInSmartMode doesn't work if we already have read access.
// We need to refactor the callers of this to require a *smart*
// read action, at which point we can remove this try-catch.
logManifestIndexQueryError(e);
}
}
val manifest = MergedManifestManager.getSnapshot(this)
return manifest.activityAttributesMap.values.asSequence()
.mapNotNull(ActivityAttributesSnapshot::getTheme)
.toSet()
}
/**
* Try to get value of theme corresponding to the given activity from {@link AndroidManifestIndex} if index is enabled.
* And it falls back to merged manifest snapshot if necessary.
*/
fun Module.getThemeNameForActivity(activityFqcn: String): String? {
if (AndroidManifestIndex.indexEnabled()) {
try {
val facet = AndroidFacet.getInstance(this)
if (facet != null) {
return DumbService.getInstance(this.project).runReadActionInSmartMode(Computable {
val activities = facet.queryActivitiesFromManifestIndex().activities
activities.asSequence()
.filter { it.qualifiedName == activityFqcn }
.mapNotNull(DefaultActivityLocator.ActivityWrapper::getTheme)
.filter { it.startsWith(SdkConstants.PREFIX_RESOURCE_REF) }
.firstOrNull()
})
}
}
catch (e: IndexNotReadyException) {
// TODO(147116755): runReadActionInSmartMode doesn't work if we already have read access.
// We need to refactor the callers of this to require a *smart*
// read action, at which point we can remove this try-catch.
logManifestIndexQueryError(e);
}
}
val manifest = MergedManifestManager.getSnapshot(this)
return manifest.getActivityAttributes(activityFqcn)
?.theme
?.takeIf { it.startsWith(SdkConstants.PREFIX_RESOURCE_REF) }
}
/**
* Returns a default theme
*/
fun Module.getDefaultTheme(renderingTarget: IAndroidTarget?, screenSize: ScreenSize?, device: Device?): String {
// For Android Wear and Android TV, the defaults differ
if (device != null) {
if (HardwareConfigHelper.isWear(device)) {
return "@android:style/Theme.DeviceDefault.Light"
}
else if (HardwareConfigHelper.isTv(device)) {
return "@style/Theme.Leanback"
}
}
// Facet being null should not happen, but has been observed to happen in rare scenarios (such as 73332530), probably
// related to race condition between Gradle sync and layout rendering
val facet = AndroidFacet.getInstance(this) ?: return SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Material.Light"
// From manifest theme documentation: "If that attribute is also not set, the default system theme is used."
val targetSdk = AndroidModuleInfo.getInstance(facet).targetSdkVersion.apiLevel
val renderingTargetSdk = renderingTarget?.version?.apiLevel ?: targetSdk
val apiLevel = targetSdk.coerceAtMost(renderingTargetSdk)
return SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX + when {
apiLevel >= 21 -> "Theme.Material.Light"
apiLevel >= 14 || apiLevel >= 11 && screenSize == ScreenSize.XLARGE -> "Theme.Holo"
else -> "Theme"
}
}