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