blob: b8c4044228962128ae2afc05297ffe8d44819fdc [file] [log] [blame]
/*
* Copyright (C) 2019 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.android.tools.idea.wizard.template
import com.google.common.annotations.VisibleForTesting
import java.util.regex.Pattern
/** Suffix added by default to activity names */
private const val ACTIVITY_NAME_SUFFIX = "Activity"
/** Suffix added by default to fragment names */
private const val FRAGMENT_NAME_SUFFIX = "Fragment"
/** Prefix added to default layout names. Can be overridden via [.overrideLayoutPrefix] */
private const val DEFAULT_LAYOUT_NAME_PREFIX = "activity"
/**
* When stripping the Activity suffix, we match against "Activity" plus zero or more digits.
* The base of the name will be captured in the first group and the digits will be captured in the second group.
*/
private val ACTIVITY_NAME_PATTERN = Regex("^(.*)$ACTIVITY_NAME_SUFFIX(\\d*)$").toPattern()
/**
* When stripping the Fragment suffix, we match against "Fragment" plus zero or more digits.
* The base of the name will be captured in the first group and the digits will be captured in the second group.
*/
private val FRAGMENT_NAME_PATTERN = Regex("^(.*)$FRAGMENT_NAME_SUFFIX(\\d*)$").toPattern()
/** Common Android system endings which we strip from class names */
@VisibleForTesting
val STRIP_CLASS_SUFFIXES = arrayOf(ACTIVITY_NAME_SUFFIX, FRAGMENT_NAME_SUFFIX, "Service", "Provider")
/**
* Strip off the end portion of the name.
* The user might be typing the activity name such that only a portion has been entered so far (e.g. "MainActivi") and we want to
* chop off that portion too such that we don't offer a layout name partially containing the activity suffix (e.g. "main_activi").
*/
tailrec fun String.stripSuffix(suffix: String, recursively: Boolean = false): String {
if (length < 2) {
return this
}
val suffixStart = lastIndexOf(suffix[0])
val name = if (suffixStart != -1 && regionMatches(suffixStart, suffix, 0, length - suffixStart))
substring(0, suffixStart)
else
this
// Recursively continue to strip the suffix (catch the FooActivityActivity case)
return if (recursively && name.endsWith(suffix)) name.stripSuffix(suffix, recursively) else name
}
/**
* Strip the "Activity" or "Fragment" suffix from a class name, e.g. "EditorActivity" -> "Editor",
* "EditorFragment" -> "Editor".
* This does not strip recursively, so "EditorActivityActivity" -> "EditorActivity"
*
* Because Studio suggests appending numbers onto new classes if they have a duplicate name,
* e.g. "MainActivity", "MainActivity2", "MainActivity3", we take that into account, for example
* we would convert "MainActivity3" into "Main3"
*/
private fun stripSuffix(name: String, suffix: String, pattern: Pattern): String {
val finalName = name.stripSuffix(suffix)
if (finalName == name) {
// pattern is expected to be either of [ACTIVITY_NAME_PATTERN] or [FRAGMENT_NAME_PATTERN], both
// have digits pattern at the end because Studio suggests appending numbers to Activity or Fragment
// classes if they have a duplicate name. This matcher is to preserve those digits.
// E.g. "MainActivity3" is converted to "Main3"
val m = pattern.matcher(name)
if (m.matches()) {
val baseName = m.group(1)
val digits = m.group(2) // May be ""
return baseName + digits
}
}
return finalName
}
private fun stripActivitySuffix(activityName: String): String = stripSuffix(activityName, ACTIVITY_NAME_SUFFIX, ACTIVITY_NAME_PATTERN)
private fun stripFragmentSuffix(fragmentName: String): String = stripSuffix(fragmentName, FRAGMENT_NAME_SUFFIX, FRAGMENT_NAME_PATTERN)
/**
* Allows a one to one mapping suggestion between different types of Android asset names, like for example mapping the name of an
* Activity to its layout. e.g. an Activity with name "ActivityMain" may have a suggested layout name of "activity_main_layout"
*/
class AssetNameConverter(private val type: Type, private val name: String) {
private var layoutPrefixOverride: String? = null
private val layoutPrefixWithTrailingUnderscore: String
get() = (if (layoutPrefixOverride == null) DEFAULT_LAYOUT_NAME_PREFIX else layoutPrefixOverride) + "_"
enum class Type {
ACTIVITY, LAYOUT, CLASS_NAME, RESOURCE, FRAGMENT
}
/**
* Convert whatever current text type we're representing into the [Type.CLASS_NAME] type,
* since that can act as a common base type we can use to reliably covert into all other types.
*/
private fun toClassName(): String = when (type) {
Type.ACTIVITY -> stripActivitySuffix(toUpperCamelCase(name))
Type.LAYOUT -> {
val layoutPrefix = layoutPrefixWithTrailingUnderscore
var layoutName = name
if (layoutName.startsWith(layoutPrefix)) {
layoutName = layoutName.substring(layoutPrefix.length)
}
underscoreToCamelCase(layoutName)
}
Type.RESOURCE -> underscoreToCamelCase(name)
Type.CLASS_NAME -> {
var className = name
// TODO(qumeric): it should not depend on the order
STRIP_CLASS_SUFFIXES.forEach {
className = className.stripSuffix(it, recursively = true)
}
if (layoutPrefixOverride != null) {
val prefixAsSuffix = underscoreToCamelCase(layoutPrefixOverride!!)
className = className.stripSuffix(prefixAsSuffix)
}
className
}
Type.FRAGMENT -> {
stripFragmentSuffix(name)
}
}
/**
* Override the default layout prefix. This should *not* include its trailing underscore.
* This will only be used when converting from or to the [Type.LAYOUT] type.
*
* Passing in `null` will clear the override, if set.
*/
fun overrideLayoutPrefix(layoutPrefixOverride: String?): AssetNameConverter {
this.layoutPrefixOverride = layoutPrefixOverride
return this
}
/**
* Takes the existing value, and converts it to the requested type.
*/
fun getValue(type: Type): String {
if (type == Type.FRAGMENT) {
overrideLayoutPrefix("fragment")
}
val className = this.toClassName()
return when (type) {
Type.ACTIVITY -> {
val activityName = extractClassName(className) ?: "Main"
activityName + ACTIVITY_NAME_SUFFIX
}
Type.LAYOUT -> {
val layoutPrefix = layoutPrefixWithTrailingUnderscore
val layoutName = camelCaseToUnderlines(className)
// We are going to add layoutNamePrefix to the result, so make sure we don't have that string already.
layoutPrefix + layoutName.replaceFirst(layoutPrefix, "", false)
}
Type.RESOURCE -> camelCaseToUnderlines(className)
Type.CLASS_NAME -> className
Type.FRAGMENT -> {
val fragmentName = extractClassName(className) ?: "Main"
fragmentName + FRAGMENT_NAME_SUFFIX
}
}
}
}