1 /* 2 * Copyright (C) 2022 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.server.devicepolicy; 18 19 import static java.util.Objects.requireNonNull; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.app.admin.DevicePolicyDrawableResource; 24 import android.app.admin.DevicePolicyResources; 25 import android.app.admin.DevicePolicyStringResource; 26 import android.app.admin.ParcelableResource; 27 import android.os.Environment; 28 import android.util.AtomicFile; 29 import android.util.Log; 30 import android.util.Xml; 31 32 import com.android.modules.utils.TypedXmlPullParser; 33 import com.android.modules.utils.TypedXmlSerializer; 34 35 import libcore.io.IoUtils; 36 37 import org.xmlpull.v1.XmlPullParserException; 38 39 import java.io.File; 40 import java.io.FileOutputStream; 41 import java.io.IOException; 42 import java.io.InputStream; 43 import java.util.HashMap; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.Objects; 47 48 /** 49 * A helper class for {@link DevicePolicyManagerService} to store/retrieve updated device 50 * management resources. 51 */ 52 class DeviceManagementResourcesProvider { 53 private static final String TAG = "DevicePolicyManagerService"; 54 55 private static final String UPDATED_RESOURCES_XML = "updated_resources.xml"; 56 private static final String TAG_ROOT = "root"; 57 private static final String TAG_DRAWABLE_STYLE_ENTRY = "drawable-style-entry"; 58 private static final String TAG_DRAWABLE_SOURCE_ENTRY = "drawable-source-entry"; 59 private static final String ATTR_DRAWABLE_STYLE = "drawable-style"; 60 private static final String ATTR_DRAWABLE_SOURCE = "drawable-source"; 61 private static final String ATTR_DRAWABLE_ID = "drawable-id"; 62 private static final String TAG_STRING_ENTRY = "string-entry"; 63 private static final String ATTR_SOURCE_ID = "source-id"; 64 65 /** 66 * Map of <drawable_id, <style_id, resource_value>> 67 */ 68 private final Map<String, Map<String, ParcelableResource>> 69 mUpdatedDrawablesForStyle = new HashMap<>(); 70 71 /** 72 * Map of <drawable_id, <source_id, <style_id, resource_value>>> 73 */ 74 private final Map<String, Map<String, Map<String, ParcelableResource>>> 75 mUpdatedDrawablesForSource = new HashMap<>(); 76 77 /** 78 * Map of <string_id, resource_value> 79 */ 80 private final Map<String, ParcelableResource> mUpdatedStrings = new HashMap<>(); 81 82 private final Object mLock = new Object(); 83 private final Injector mInjector; 84 DeviceManagementResourcesProvider()85 DeviceManagementResourcesProvider() { 86 this(new Injector()); 87 } 88 DeviceManagementResourcesProvider(Injector injector)89 DeviceManagementResourcesProvider(Injector injector) { 90 mInjector = requireNonNull(injector); 91 } 92 93 /** 94 * Returns {@code false} if no resources were updated. 95 */ updateDrawables(@onNull List<DevicePolicyDrawableResource> drawables)96 boolean updateDrawables(@NonNull List<DevicePolicyDrawableResource> drawables) { 97 boolean updated = false; 98 for (int i = 0; i < drawables.size(); i++) { 99 String drawableId = drawables.get(i).getDrawableId(); 100 String drawableStyle = drawables.get(i).getDrawableStyle(); 101 String drawableSource = drawables.get(i).getDrawableSource(); 102 ParcelableResource resource = drawables.get(i).getResource(); 103 104 Objects.requireNonNull(drawableId, "drawableId must be provided."); 105 Objects.requireNonNull(drawableStyle, "drawableStyle must be provided."); 106 Objects.requireNonNull(drawableSource, "drawableSource must be provided."); 107 Objects.requireNonNull(resource, "ParcelableResource must be provided."); 108 109 if (DevicePolicyResources.UNDEFINED.equals(drawableSource)) { 110 updated |= updateDrawable(drawableId, drawableStyle, resource); 111 } else { 112 updated |= updateDrawableForSource( 113 drawableId, drawableSource, drawableStyle, resource); 114 } 115 } 116 if (!updated) { 117 return false; 118 } 119 synchronized (mLock) { 120 write(); 121 return true; 122 } 123 } 124 updateDrawable( String drawableId, String drawableStyle, ParcelableResource updatableResource)125 private boolean updateDrawable( 126 String drawableId, String drawableStyle, ParcelableResource updatableResource) { 127 synchronized (mLock) { 128 if (!mUpdatedDrawablesForStyle.containsKey(drawableId)) { 129 mUpdatedDrawablesForStyle.put(drawableId, new HashMap<>()); 130 } 131 ParcelableResource current = mUpdatedDrawablesForStyle.get(drawableId).get( 132 drawableStyle); 133 if (updatableResource.equals(current)) { 134 return false; 135 } 136 mUpdatedDrawablesForStyle.get(drawableId).put(drawableStyle, updatableResource); 137 return true; 138 } 139 } 140 updateDrawableForSource( String drawableId, String drawableSource, String drawableStyle, ParcelableResource updatableResource)141 private boolean updateDrawableForSource( 142 String drawableId, String drawableSource, String drawableStyle, 143 ParcelableResource updatableResource) { 144 synchronized (mLock) { 145 if (!mUpdatedDrawablesForSource.containsKey(drawableId)) { 146 mUpdatedDrawablesForSource.put(drawableId, new HashMap<>()); 147 } 148 Map<String, Map<String, ParcelableResource>> drawablesForId = 149 mUpdatedDrawablesForSource.get(drawableId); 150 if (!drawablesForId.containsKey(drawableSource)) { 151 mUpdatedDrawablesForSource.get(drawableId).put(drawableSource, new HashMap<>()); 152 } 153 ParcelableResource current = drawablesForId.get(drawableSource).get(drawableStyle); 154 if (updatableResource.equals(current)) { 155 return false; 156 } 157 drawablesForId.get(drawableSource).put(drawableStyle, updatableResource); 158 return true; 159 } 160 } 161 162 /** 163 * Returns {@code false} if no resources were removed. 164 */ removeDrawables(@onNull List<String> drawableIds)165 boolean removeDrawables(@NonNull List<String> drawableIds) { 166 synchronized (mLock) { 167 boolean removed = false; 168 for (int i = 0; i < drawableIds.size(); i++) { 169 String drawableId = drawableIds.get(i); 170 removed |= mUpdatedDrawablesForStyle.remove(drawableId) != null 171 || mUpdatedDrawablesForSource.remove(drawableId) != null; 172 } 173 if (!removed) { 174 return false; 175 } 176 write(); 177 return true; 178 } 179 } 180 181 @Nullable getDrawable(String drawableId, String drawableStyle, String drawableSource)182 ParcelableResource getDrawable(String drawableId, String drawableStyle, String drawableSource) { 183 synchronized (mLock) { 184 ParcelableResource resource = getDrawableForSourceLocked( 185 drawableId, drawableStyle, drawableSource); 186 if (resource != null) { 187 return resource; 188 } 189 if (!mUpdatedDrawablesForStyle.containsKey(drawableId)) { 190 return null; 191 } 192 return mUpdatedDrawablesForStyle.get(drawableId).get(drawableStyle); 193 } 194 } 195 196 @Nullable getDrawableForSourceLocked( String drawableId, String drawableStyle, String drawableSource)197 ParcelableResource getDrawableForSourceLocked( 198 String drawableId, String drawableStyle, String drawableSource) { 199 if (!mUpdatedDrawablesForSource.containsKey(drawableId)) { 200 return null; 201 } 202 if (!mUpdatedDrawablesForSource.get(drawableId).containsKey(drawableSource)) { 203 return null; 204 } 205 return mUpdatedDrawablesForSource.get(drawableId).get(drawableSource).get(drawableStyle); 206 } 207 208 /** 209 * Returns {@code false} if no resources were updated. 210 */ updateStrings(@onNull List<DevicePolicyStringResource> strings)211 boolean updateStrings(@NonNull List<DevicePolicyStringResource> strings) { 212 boolean updated = false; 213 for (int i = 0; i < strings.size(); i++) { 214 String stringId = strings.get(i).getStringId(); 215 ParcelableResource resource = strings.get(i).getResource(); 216 217 Objects.requireNonNull(stringId, "stringId must be provided."); 218 Objects.requireNonNull(resource, "ParcelableResource must be provided."); 219 220 updated |= updateString(stringId, resource); 221 } 222 if (!updated) { 223 return false; 224 } 225 synchronized (mLock) { 226 write(); 227 return true; 228 } 229 } 230 updateString(String stringId, ParcelableResource updatableResource)231 private boolean updateString(String stringId, ParcelableResource updatableResource) { 232 synchronized (mLock) { 233 ParcelableResource current = mUpdatedStrings.get(stringId); 234 if (updatableResource.equals(current)) { 235 return false; 236 } 237 mUpdatedStrings.put(stringId, updatableResource); 238 return true; 239 } 240 } 241 242 /** 243 * Returns {@code false} if no resources were removed. 244 */ removeStrings(@onNull List<String> stringIds)245 boolean removeStrings(@NonNull List<String> stringIds) { 246 synchronized (mLock) { 247 boolean removed = false; 248 for (int i = 0; i < stringIds.size(); i++) { 249 String stringId = stringIds.get(i); 250 removed |= mUpdatedStrings.remove(stringId) != null; 251 } 252 if (!removed) { 253 return false; 254 } 255 write(); 256 return true; 257 } 258 } 259 260 @Nullable getString(String stringId)261 ParcelableResource getString(String stringId) { 262 synchronized (mLock) { 263 return mUpdatedStrings.get(stringId); 264 } 265 } 266 write()267 private void write() { 268 Log.d(TAG, "Writing updated resources to file."); 269 new ResourcesReaderWriter().writeToFileLocked(); 270 } 271 load()272 void load() { 273 synchronized (mLock) { 274 new ResourcesReaderWriter().readFromFileLocked(); 275 } 276 } 277 getResourcesFile()278 private File getResourcesFile() { 279 return new File(mInjector.environmentGetDataSystemDirectory(), UPDATED_RESOURCES_XML); 280 } 281 282 private class ResourcesReaderWriter { 283 private final File mFile; ResourcesReaderWriter()284 private ResourcesReaderWriter() { 285 mFile = getResourcesFile(); 286 } 287 writeToFileLocked()288 void writeToFileLocked() { 289 Log.d(TAG, "Writing to " + mFile); 290 291 AtomicFile f = new AtomicFile(mFile); 292 FileOutputStream outputStream = null; 293 try { 294 outputStream = f.startWrite(); 295 TypedXmlSerializer out = Xml.resolveSerializer(outputStream); 296 297 // Root tag 298 out.startDocument(null, true); 299 out.startTag(null, TAG_ROOT); 300 301 // Actual content 302 writeInner(out); 303 304 // Close root 305 out.endTag(null, TAG_ROOT); 306 out.endDocument(); 307 out.flush(); 308 309 // Commit the content. 310 f.finishWrite(outputStream); 311 outputStream = null; 312 313 } catch (IOException e) { 314 Log.e(TAG, "Exception when writing", e); 315 if (outputStream != null) { 316 f.failWrite(outputStream); 317 } 318 } 319 } 320 readFromFileLocked()321 void readFromFileLocked() { 322 if (!mFile.exists()) { 323 Log.d(TAG, "" + mFile + " doesn't exist"); 324 return; 325 } 326 327 Log.d(TAG, "Reading from " + mFile); 328 AtomicFile f = new AtomicFile(mFile); 329 InputStream input = null; 330 try { 331 input = f.openRead(); 332 TypedXmlPullParser parser = Xml.resolvePullParser(input); 333 334 int type; 335 int depth = 0; 336 while ((type = parser.next()) != TypedXmlPullParser.END_DOCUMENT) { 337 switch (type) { 338 case TypedXmlPullParser.START_TAG: 339 depth++; 340 break; 341 case TypedXmlPullParser.END_TAG: 342 depth--; 343 // fallthrough 344 default: 345 continue; 346 } 347 // Check the root tag 348 String tag = parser.getName(); 349 if (depth == 1) { 350 if (!TAG_ROOT.equals(tag)) { 351 Log.e(TAG, "Invalid root tag: " + tag); 352 return; 353 } 354 continue; 355 } 356 // readInner() will only see START_TAG at depth >= 2. 357 if (!readInner(parser, depth, tag)) { 358 return; // Error 359 } 360 } 361 } catch (XmlPullParserException | IOException e) { 362 Log.e(TAG, "Error parsing resources file", e); 363 } finally { 364 IoUtils.closeQuietly(input); 365 } 366 } 367 writeInner(TypedXmlSerializer out)368 void writeInner(TypedXmlSerializer out) throws IOException { 369 writeDrawablesForStylesInner(out); 370 writeDrawablesForSourcesInner(out); 371 writeStringsInner(out); 372 } 373 writeDrawablesForStylesInner(TypedXmlSerializer out)374 private void writeDrawablesForStylesInner(TypedXmlSerializer out) throws IOException { 375 if (mUpdatedDrawablesForStyle != null && !mUpdatedDrawablesForStyle.isEmpty()) { 376 for (Map.Entry<String, Map<String, ParcelableResource>> drawableEntry 377 : mUpdatedDrawablesForStyle.entrySet()) { 378 for (Map.Entry<String, ParcelableResource> styleEntry 379 : drawableEntry.getValue().entrySet()) { 380 out.startTag(/* namespace= */ null, TAG_DRAWABLE_STYLE_ENTRY); 381 out.attribute( 382 /* namespace= */ null, ATTR_DRAWABLE_ID, drawableEntry.getKey()); 383 out.attribute( 384 /* namespace= */ null, 385 ATTR_DRAWABLE_STYLE, 386 styleEntry.getKey()); 387 styleEntry.getValue().writeToXmlFile(out); 388 out.endTag(/* namespace= */ null, TAG_DRAWABLE_STYLE_ENTRY); 389 } 390 } 391 } 392 } 393 writeDrawablesForSourcesInner(TypedXmlSerializer out)394 private void writeDrawablesForSourcesInner(TypedXmlSerializer out) throws IOException { 395 if (mUpdatedDrawablesForSource != null && !mUpdatedDrawablesForSource.isEmpty()) { 396 for (Map.Entry<String, Map<String, Map<String, ParcelableResource>>> drawableEntry 397 : mUpdatedDrawablesForSource.entrySet()) { 398 for (Map.Entry<String, Map<String, ParcelableResource>> sourceEntry 399 : drawableEntry.getValue().entrySet()) { 400 for (Map.Entry<String, ParcelableResource> styleEntry 401 : sourceEntry.getValue().entrySet()) { 402 out.startTag(/* namespace= */ null, TAG_DRAWABLE_SOURCE_ENTRY); 403 out.attribute(/* namespace= */ null, ATTR_DRAWABLE_ID, 404 drawableEntry.getKey()); 405 out.attribute(/* namespace= */ null, ATTR_DRAWABLE_SOURCE, 406 sourceEntry.getKey()); 407 out.attribute(/* namespace= */ null, ATTR_DRAWABLE_STYLE, 408 styleEntry.getKey()); 409 styleEntry.getValue().writeToXmlFile(out); 410 out.endTag(/* namespace= */ null, TAG_DRAWABLE_SOURCE_ENTRY); 411 } 412 } 413 } 414 } 415 } 416 writeStringsInner(TypedXmlSerializer out)417 private void writeStringsInner(TypedXmlSerializer out) throws IOException { 418 if (mUpdatedStrings != null && !mUpdatedStrings.isEmpty()) { 419 for (Map.Entry<String, ParcelableResource> entry 420 : mUpdatedStrings.entrySet()) { 421 out.startTag(/* namespace= */ null, TAG_STRING_ENTRY); 422 out.attribute( 423 /* namespace= */ null, 424 ATTR_SOURCE_ID, 425 entry.getKey()); 426 entry.getValue().writeToXmlFile(out); 427 out.endTag(/* namespace= */ null, TAG_STRING_ENTRY); 428 } 429 } 430 } 431 readInner(TypedXmlPullParser parser, int depth, String tag)432 private boolean readInner(TypedXmlPullParser parser, int depth, String tag) 433 throws XmlPullParserException, IOException { 434 if (depth > 2) { 435 return true; // Ignore 436 } 437 switch (tag) { 438 case TAG_DRAWABLE_STYLE_ENTRY: { 439 String id = parser.getAttributeValue(/* namespace= */ null, ATTR_DRAWABLE_ID); 440 String style = parser.getAttributeValue( 441 /* namespace= */ null, ATTR_DRAWABLE_STYLE); 442 ParcelableResource resource = ParcelableResource.createFromXml(parser); 443 if (!mUpdatedDrawablesForStyle.containsKey(id)) { 444 mUpdatedDrawablesForStyle.put(id, new HashMap<>()); 445 } 446 mUpdatedDrawablesForStyle.get(id).put(style, resource); 447 break; 448 } 449 case TAG_DRAWABLE_SOURCE_ENTRY: { 450 String id = parser.getAttributeValue(/* namespace= */ null, ATTR_DRAWABLE_ID); 451 String source = parser.getAttributeValue( 452 /* namespace= */ null, ATTR_DRAWABLE_SOURCE); 453 String style = parser.getAttributeValue( 454 /* namespace= */ null, ATTR_DRAWABLE_STYLE); 455 ParcelableResource resource = ParcelableResource.createFromXml(parser); 456 if (!mUpdatedDrawablesForSource.containsKey(id)) { 457 mUpdatedDrawablesForSource.put(id, new HashMap<>()); 458 } 459 if (!mUpdatedDrawablesForSource.get(id).containsKey(source)) { 460 mUpdatedDrawablesForSource.get(id).put(source, new HashMap<>()); 461 } 462 mUpdatedDrawablesForSource.get(id).get(source).put(style, resource); 463 break; 464 } 465 case TAG_STRING_ENTRY: { 466 String id = parser.getAttributeValue(/* namespace= */ null, ATTR_SOURCE_ID); 467 mUpdatedStrings.put(id, ParcelableResource.createFromXml(parser)); 468 break; 469 } 470 default: { 471 Log.e(TAG, "Unexpected tag: " + tag); 472 return false; 473 } 474 } 475 return true; 476 } 477 } 478 479 public static class Injector { environmentGetDataSystemDirectory()480 File environmentGetDataSystemDirectory() { 481 return Environment.getDataSystemDirectory(); 482 } 483 } 484 } 485