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