1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3.responsive
18 
19 import android.content.res.TypedArray
20 import android.util.Log
21 import com.android.launcher3.R
22 import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
23 import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
24 import com.android.launcher3.util.ResourceHelper
25 
26 class HotseatSpecsProvider(groupOfSpecs: List<ResponsiveSpecGroup<HotseatSpec>>) {
27 
28     private val groupOfSpecs: List<ResponsiveSpecGroup<HotseatSpec>>
29 
30     init {
<lambda>null31         this.groupOfSpecs = groupOfSpecs.sortedBy { it.aspectRatio }
32     }
33 
getSpecsByAspectRationull34     fun getSpecsByAspectRatio(aspectRatio: Float): ResponsiveSpecGroup<HotseatSpec> {
35         check(aspectRatio > 0f) { "Invalid aspect ratio! The value should be bigger than 0." }
36 
37         val specsGroup = groupOfSpecs.firstOrNull { aspectRatio <= it.aspectRatio }
38         check(specsGroup != null) { "No available spec with aspectRatio within $aspectRatio." }
39 
40         return specsGroup
41     }
42 
getSpecIgnoringDimensionTypenull43     private fun getSpecIgnoringDimensionType(
44         availableSize: Int,
45         specsGroup: ResponsiveSpecGroup<HotseatSpec>
46     ): HotseatSpec? {
47         val specWidth = specsGroup.widthSpecs.firstOrNull { availableSize <= it.maxAvailableSize }
48         val specHeight = specsGroup.heightSpecs.firstOrNull { availableSize <= it.maxAvailableSize }
49         return specWidth ?: specHeight
50     }
51 
getCalculatedSpecnull52     fun getCalculatedSpec(
53         aspectRatio: Float,
54         dimensionType: DimensionType,
55         availableSpace: Int,
56     ): CalculatedHotseatSpec {
57         val specsGroup = getSpecsByAspectRatio(aspectRatio)
58 
59         // TODO(b/315548992): Ignore the dimension type to prevent crash before launcher
60         //  data migration is finished. The restore process allows the initialization of
61         //  an invalid or disabled grid until the data is restored and migrated.
62         val spec = getSpecIgnoringDimensionType(availableSpace, specsGroup)
63         check(spec != null) { "No available spec found within $availableSpace. $specsGroup" }
64         // val spec = specsGroup.getSpec(dimensionType, availableSpace)
65         return CalculatedHotseatSpec(availableSpace, spec)
66     }
67 
68     companion object {
69         @JvmStatic
createnull70         fun create(resourceHelper: ResourceHelper): HotseatSpecsProvider {
71             val parser = ResponsiveSpecsParser(resourceHelper)
72             val specs = parser.parseXML(ResponsiveSpecType.Hotseat, ::HotseatSpec)
73             return HotseatSpecsProvider(specs)
74         }
75     }
76 }
77 
78 data class HotseatSpec(
79     override val maxAvailableSize: Int,
80     override val dimensionType: DimensionType,
81     override val specType: ResponsiveSpecType,
82     val hotseatQsbSpace: SizeSpec,
83     val edgePadding: SizeSpec
84 ) : IResponsiveSpec {
85     init {
<lambda>null86         check(isValid()) { "Invalid HotseatSpec found." }
87     }
88 
89     constructor(
90         responsiveSpecType: ResponsiveSpecType,
91         attrs: TypedArray,
92         specs: Map<String, SizeSpec>
93     ) : this(
94         maxAvailableSize =
95             attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
96         dimensionType =
97             DimensionType.entries[
98                     attrs.getInt(
99                         R.styleable.ResponsiveSpec_dimensionType,
100                         DimensionType.HEIGHT.ordinal
101                     )],
102         specType = responsiveSpecType,
103         hotseatQsbSpace = specs.getOrError(SizeSpec.XmlTags.HOTSEAT_QSB_SPACE),
104         edgePadding = specs.getOrError(SizeSpec.XmlTags.EDGE_PADDING)
105     )
106 
isValidnull107     fun isValid(): Boolean {
108         if (maxAvailableSize <= 0) {
109             logError("The property maxAvailableSize must be higher than 0.")
110             return false
111         }
112 
113         // All specs need to be individually valid
114         if (!allSpecsAreValid()) {
115             logError("One or more specs are invalid!")
116             return false
117         }
118 
119         if (!isValidFixedSize()) {
120             logError("The total Fixed Size used must be lower or equal to $maxAvailableSize.")
121             return false
122         }
123 
124         return true
125     }
126 
allSpecsAreValidnull127     private fun allSpecsAreValid(): Boolean {
128         return hotseatQsbSpace.isValid() &&
129             hotseatQsbSpace.onlyFixedSize() &&
130             edgePadding.isValid() &&
131             edgePadding.onlyFixedSize()
132     }
133 
isValidFixedSizenull134     private fun isValidFixedSize() =
135         hotseatQsbSpace.fixedSize + edgePadding.fixedSize <= maxAvailableSize
136 
137     private fun logError(message: String) {
138         Log.e(LOG_TAG, "$LOG_TAG #isValid - $message - $this")
139     }
140 
141     companion object {
142         private const val LOG_TAG = "HotseatSpec"
143     }
144 }
145 
146 class CalculatedHotseatSpec(val availableSpace: Int, val spec: HotseatSpec) {
147 
148     var hotseatQsbSpace: Int = 0
149         private set
150 
151     var edgePadding: Int = 0
152         private set
153 
154     init {
155         hotseatQsbSpace = spec.hotseatQsbSpace.getCalculatedValue(availableSpace)
156         edgePadding = spec.edgePadding.getCalculatedValue(availableSpace)
157     }
158 
hashCodenull159     override fun hashCode(): Int {
160         var result = availableSpace.hashCode()
161         result = 31 * result + hotseatQsbSpace.hashCode()
162         result = 31 * result + edgePadding.hashCode()
163         result = 31 * result + spec.hashCode()
164         return result
165     }
166 
equalsnull167     override fun equals(other: Any?): Boolean {
168         return other is CalculatedHotseatSpec &&
169             availableSpace == other.availableSpace &&
170             hotseatQsbSpace == other.hotseatQsbSpace &&
171             edgePadding == other.edgePadding &&
172             spec == other.spec
173     }
174 
toStringnull175     override fun toString(): String {
176         return "${this::class.simpleName}(" +
177             "availableSpace=$availableSpace, hotseatQsbSpace=$hotseatQsbSpace, " +
178             "edgePadding=$edgePadding, " +
179             "${spec::class.simpleName}.maxAvailableSize=${spec.maxAvailableSize}" +
180             ")"
181     }
182 }
183