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.workspace 18 19 import android.content.res.TypedArray 20 import android.content.res.XmlResourceParser 21 import android.util.AttributeSet 22 import android.util.Log 23 import android.util.TypedValue 24 import android.util.Xml 25 import com.android.launcher3.R 26 import com.android.launcher3.util.ResourceHelper 27 import java.io.IOException 28 import org.xmlpull.v1.XmlPullParser 29 import org.xmlpull.v1.XmlPullParserException 30 31 private const val TAG = "WorkspaceSpecs" 32 33 class WorkspaceSpecs(resourceHelper: ResourceHelper) { 34 object XmlTags { 35 const val WORKSPACE_SPECS = "workspaceSpecs" 36 37 const val WORKSPACE_SPEC = "workspaceSpec" 38 const val START_PADDING = "startPadding" 39 const val END_PADDING = "endPadding" 40 const val GUTTER = "gutter" 41 const val CELL_SIZE = "cellSize" 42 } 43 44 val workspaceHeightSpecList = mutableListOf<WorkspaceSpec>() 45 val workspaceWidthSpecList = mutableListOf<WorkspaceSpec>() 46 47 init { 48 try { 49 val parser: XmlResourceParser = resourceHelper.getXml() 50 val depth = parser.depth 51 var type: Int 52 while ( <lambda>null53 (parser.next().also { type = it } != XmlPullParser.END_TAG || 54 parser.depth > depth) && type != XmlPullParser.END_DOCUMENT 55 ) { 56 if (type == XmlPullParser.START_TAG && XmlTags.WORKSPACE_SPECS == parser.name) { 57 val displayDepth = parser.depth 58 while ( <lambda>null59 (parser.next().also { type = it } != XmlPullParser.END_TAG || 60 parser.depth > displayDepth) && type != XmlPullParser.END_DOCUMENT 61 ) { 62 if ( 63 type == XmlPullParser.START_TAG && XmlTags.WORKSPACE_SPEC == parser.name 64 ) { 65 val attrs = 66 resourceHelper.obtainStyledAttributes( 67 Xml.asAttributeSet(parser), 68 R.styleable.WorkspaceSpec 69 ) 70 val maxAvailableSize = 71 attrs.getDimensionPixelSize( 72 R.styleable.WorkspaceSpec_maxAvailableSize, 73 0 74 ) 75 val specType = 76 WorkspaceSpec.SpecType.values()[ 77 attrs.getInt( 78 R.styleable.WorkspaceSpec_specType, 79 WorkspaceSpec.SpecType.HEIGHT.ordinal 80 )] 81 attrs.recycle() 82 83 var startPadding: SizeSpec? = null 84 var endPadding: SizeSpec? = null 85 var gutter: SizeSpec? = null 86 var cellSize: SizeSpec? = null 87 88 val limitDepth = parser.depth 89 while ( <lambda>null90 (parser.next().also { type = it } != XmlPullParser.END_TAG || 91 parser.depth > limitDepth) && type != XmlPullParser.END_DOCUMENT 92 ) { 93 val attr: AttributeSet = Xml.asAttributeSet(parser) 94 if (type == XmlPullParser.START_TAG) { 95 when (parser.name) { 96 XmlTags.START_PADDING -> { 97 startPadding = SizeSpec(resourceHelper, attr) 98 } 99 XmlTags.END_PADDING -> { 100 endPadding = SizeSpec(resourceHelper, attr) 101 } 102 XmlTags.GUTTER -> { 103 gutter = SizeSpec(resourceHelper, attr) 104 } 105 XmlTags.CELL_SIZE -> { 106 cellSize = SizeSpec(resourceHelper, attr) 107 } 108 } 109 } 110 } 111 112 if ( 113 startPadding == null || 114 endPadding == null || 115 gutter == null || 116 cellSize == null 117 ) { 118 throw IllegalStateException( 119 "All attributes in workspaceSpec must be defined" 120 ) 121 } 122 123 val workspaceSpec = 124 WorkspaceSpec( 125 maxAvailableSize, 126 specType, 127 startPadding, 128 endPadding, 129 gutter, 130 cellSize 131 ) 132 if (workspaceSpec.isValid()) { 133 if (workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT) 134 workspaceHeightSpecList.add(workspaceSpec) 135 else workspaceWidthSpecList.add(workspaceSpec) 136 } else { 137 throw IllegalStateException("Invalid workspaceSpec found.") 138 } 139 } 140 } 141 142 if (workspaceWidthSpecList.isEmpty() || workspaceHeightSpecList.isEmpty()) { 143 throw IllegalStateException( 144 "WorkspaceSpecs is incomplete - " + 145 "height list size = ${workspaceHeightSpecList.size}; " + 146 "width list size = ${workspaceWidthSpecList.size}." 147 ) 148 } 149 } 150 } 151 parser.close() 152 } catch (e: Exception) { 153 when (e) { 154 is IOException, 155 is XmlPullParserException -> { 156 throw RuntimeException("Failure parsing workspaces specs file.", e) 157 } 158 else -> throw e 159 } 160 } 161 } 162 } 163 164 data class WorkspaceSpec( 165 val maxAvailableSize: Int, 166 val specType: SpecType, 167 val startPadding: SizeSpec, 168 val endPadding: SizeSpec, 169 val gutter: SizeSpec, 170 val cellSize: SizeSpec 171 ) { 172 173 enum class SpecType { 174 HEIGHT, 175 WIDTH 176 } 177 isValidnull178 fun isValid(): Boolean { 179 if (maxAvailableSize <= 0) { 180 Log.e(TAG, "WorkspaceSpec#isValid - maxAvailableSize <= 0") 181 return false 182 } 183 184 // All specs need to be individually valid 185 if (!allSpecsAreValid()) { 186 Log.e(TAG, "WorkspaceSpec#isValid - !allSpecsAreValid()") 187 return false 188 } 189 190 return true 191 } 192 allSpecsAreValidnull193 private fun allSpecsAreValid(): Boolean = 194 startPadding.isValid() && endPadding.isValid() && gutter.isValid() && cellSize.isValid() 195 } 196 197 class SizeSpec(resourceHelper: ResourceHelper, attrs: AttributeSet) { 198 val fixedSize: Float 199 val ofAvailableSpace: Float 200 val ofRemainderSpace: Float 201 202 init { 203 val styledAttrs = resourceHelper.obtainStyledAttributes(attrs, R.styleable.SpecSize) 204 205 fixedSize = getValue(styledAttrs, R.styleable.SpecSize_fixedSize) 206 ofAvailableSpace = getValue(styledAttrs, R.styleable.SpecSize_ofAvailableSpace) 207 ofRemainderSpace = getValue(styledAttrs, R.styleable.SpecSize_ofRemainderSpace) 208 209 styledAttrs.recycle() 210 } 211 212 private fun getValue(a: TypedArray, index: Int): Float { 213 if (a.getType(index) == TypedValue.TYPE_DIMENSION) { 214 return a.getDimensionPixelSize(index, 0).toFloat() 215 } else if (a.getType(index) == TypedValue.TYPE_FLOAT) { 216 return a.getFloat(index, 0f) 217 } 218 return 0f 219 } 220 221 fun isValid(): Boolean { 222 // All attributes are empty 223 if (fixedSize <= 0f && ofAvailableSpace <= 0f && ofRemainderSpace <= 0f) { 224 Log.e(TAG, "SizeSpec#isValid - all attributes are empty") 225 return false 226 } 227 228 // More than one attribute is filled 229 val attrCount = 230 (if (fixedSize > 0) 1 else 0) + 231 (if (ofAvailableSpace > 0) 1 else 0) + 232 (if (ofRemainderSpace > 0) 1 else 0) 233 if (attrCount > 1) { 234 Log.e(TAG, "SizeSpec#isValid - more than one attribute is filled") 235 return false 236 } 237 238 // Values should be between 0 and 1 239 if (ofAvailableSpace !in 0f..1f || ofRemainderSpace !in 0f..1f) { 240 Log.e(TAG, "SizeSpec#isValid - values should be between 0 and 1") 241 return false 242 } 243 244 return true 245 } 246 247 override fun toString(): String { 248 return "SizeSpec(fixedSize=$fixedSize, ofAvailableSpace=$ofAvailableSpace, " + 249 "ofRemainderSpace=$ofRemainderSpace)" 250 } 251 } 252