1 /*
2  * Copyright (C) 2015 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 android.os.storage;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.res.Resources;
24 import android.net.Uri;
25 import android.os.Environment;
26 import android.os.Parcel;
27 import android.os.Parcelable;
28 import android.os.UserHandle;
29 import android.provider.DocumentsContract;
30 import android.text.TextUtils;
31 import android.util.ArrayMap;
32 import android.util.DebugUtils;
33 import android.util.SparseArray;
34 import android.util.SparseIntArray;
35 
36 import com.android.internal.R;
37 import com.android.internal.util.IndentingPrintWriter;
38 import com.android.internal.util.Preconditions;
39 
40 import java.io.CharArrayWriter;
41 import java.io.File;
42 import java.util.Comparator;
43 import java.util.Objects;
44 
45 /**
46  * Information about a storage volume that may be mounted. A volume may be a
47  * partition on a physical {@link DiskInfo}, an emulated volume above some other
48  * storage medium, or a standalone container like an ASEC or OBB.
49  * <p>
50  * Volumes may be mounted with various flags:
51  * <ul>
52  * <li>{@link #MOUNT_FLAG_PRIMARY} means the volume provides primary external
53  * storage, historically found at {@code /sdcard}.
54  * <li>{@link #MOUNT_FLAG_VISIBLE} means the volume is visible to third-party
55  * apps for direct filesystem access. The system should send out relevant
56  * storage broadcasts and index any media on visible volumes. Visible volumes
57  * are considered a more stable part of the device, which is why we take the
58  * time to index them. In particular, transient volumes like USB OTG devices
59  * <em>should not</em> be marked as visible; their contents should be surfaced
60  * to apps through the Storage Access Framework.
61  * </ul>
62  *
63  * @hide
64  */
65 public class VolumeInfo implements Parcelable {
66     public static final String ACTION_VOLUME_STATE_CHANGED =
67             "android.os.storage.action.VOLUME_STATE_CHANGED";
68     public static final String EXTRA_VOLUME_ID =
69             "android.os.storage.extra.VOLUME_ID";
70     public static final String EXTRA_VOLUME_STATE =
71             "android.os.storage.extra.VOLUME_STATE";
72 
73     /** Stub volume representing internal private storage */
74     public static final String ID_PRIVATE_INTERNAL = "private";
75     /** Real volume representing internal emulated storage */
76     public static final String ID_EMULATED_INTERNAL = "emulated";
77 
78     public static final int TYPE_PUBLIC = 0;
79     public static final int TYPE_PRIVATE = 1;
80     public static final int TYPE_EMULATED = 2;
81     public static final int TYPE_ASEC = 3;
82     public static final int TYPE_OBB = 4;
83 
84     public static final int STATE_UNMOUNTED = 0;
85     public static final int STATE_CHECKING = 1;
86     public static final int STATE_MOUNTED = 2;
87     public static final int STATE_MOUNTED_READ_ONLY = 3;
88     public static final int STATE_FORMATTING = 4;
89     public static final int STATE_EJECTING = 5;
90     public static final int STATE_UNMOUNTABLE = 6;
91     public static final int STATE_REMOVED = 7;
92     public static final int STATE_BAD_REMOVAL = 8;
93 
94     public static final int MOUNT_FLAG_PRIMARY = 1 << 0;
95     public static final int MOUNT_FLAG_VISIBLE = 1 << 1;
96 
97     private static SparseArray<String> sStateToEnvironment = new SparseArray<>();
98     private static ArrayMap<String, String> sEnvironmentToBroadcast = new ArrayMap<>();
99     private static SparseIntArray sStateToDescrip = new SparseIntArray();
100 
101     private static final Comparator<VolumeInfo>
102             sDescriptionComparator = new Comparator<VolumeInfo>() {
103         @Override
104         public int compare(VolumeInfo lhs, VolumeInfo rhs) {
105             if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(lhs.getId())) {
106                 return -1;
107             } else if (lhs.getDescription() == null) {
108                 return 1;
109             } else if (rhs.getDescription() == null) {
110                 return -1;
111             } else {
112                 return lhs.getDescription().compareTo(rhs.getDescription());
113             }
114         }
115     };
116 
117     static {
sStateToEnvironment.put(VolumeInfo.STATE_UNMOUNTED, Environment.MEDIA_UNMOUNTED)118         sStateToEnvironment.put(VolumeInfo.STATE_UNMOUNTED, Environment.MEDIA_UNMOUNTED);
sStateToEnvironment.put(VolumeInfo.STATE_CHECKING, Environment.MEDIA_CHECKING)119         sStateToEnvironment.put(VolumeInfo.STATE_CHECKING, Environment.MEDIA_CHECKING);
sStateToEnvironment.put(VolumeInfo.STATE_MOUNTED, Environment.MEDIA_MOUNTED)120         sStateToEnvironment.put(VolumeInfo.STATE_MOUNTED, Environment.MEDIA_MOUNTED);
sStateToEnvironment.put(VolumeInfo.STATE_MOUNTED_READ_ONLY, Environment.MEDIA_MOUNTED_READ_ONLY)121         sStateToEnvironment.put(VolumeInfo.STATE_MOUNTED_READ_ONLY, Environment.MEDIA_MOUNTED_READ_ONLY);
sStateToEnvironment.put(VolumeInfo.STATE_FORMATTING, Environment.MEDIA_UNMOUNTED)122         sStateToEnvironment.put(VolumeInfo.STATE_FORMATTING, Environment.MEDIA_UNMOUNTED);
sStateToEnvironment.put(VolumeInfo.STATE_EJECTING, Environment.MEDIA_EJECTING)123         sStateToEnvironment.put(VolumeInfo.STATE_EJECTING, Environment.MEDIA_EJECTING);
sStateToEnvironment.put(VolumeInfo.STATE_UNMOUNTABLE, Environment.MEDIA_UNMOUNTABLE)124         sStateToEnvironment.put(VolumeInfo.STATE_UNMOUNTABLE, Environment.MEDIA_UNMOUNTABLE);
sStateToEnvironment.put(VolumeInfo.STATE_REMOVED, Environment.MEDIA_REMOVED)125         sStateToEnvironment.put(VolumeInfo.STATE_REMOVED, Environment.MEDIA_REMOVED);
sStateToEnvironment.put(VolumeInfo.STATE_BAD_REMOVAL, Environment.MEDIA_BAD_REMOVAL)126         sStateToEnvironment.put(VolumeInfo.STATE_BAD_REMOVAL, Environment.MEDIA_BAD_REMOVAL);
127 
sEnvironmentToBroadcast.put(Environment.MEDIA_UNMOUNTED, Intent.ACTION_MEDIA_UNMOUNTED)128         sEnvironmentToBroadcast.put(Environment.MEDIA_UNMOUNTED, Intent.ACTION_MEDIA_UNMOUNTED);
sEnvironmentToBroadcast.put(Environment.MEDIA_CHECKING, Intent.ACTION_MEDIA_CHECKING)129         sEnvironmentToBroadcast.put(Environment.MEDIA_CHECKING, Intent.ACTION_MEDIA_CHECKING);
sEnvironmentToBroadcast.put(Environment.MEDIA_MOUNTED, Intent.ACTION_MEDIA_MOUNTED)130         sEnvironmentToBroadcast.put(Environment.MEDIA_MOUNTED, Intent.ACTION_MEDIA_MOUNTED);
sEnvironmentToBroadcast.put(Environment.MEDIA_MOUNTED_READ_ONLY, Intent.ACTION_MEDIA_MOUNTED)131         sEnvironmentToBroadcast.put(Environment.MEDIA_MOUNTED_READ_ONLY, Intent.ACTION_MEDIA_MOUNTED);
sEnvironmentToBroadcast.put(Environment.MEDIA_EJECTING, Intent.ACTION_MEDIA_EJECT)132         sEnvironmentToBroadcast.put(Environment.MEDIA_EJECTING, Intent.ACTION_MEDIA_EJECT);
sEnvironmentToBroadcast.put(Environment.MEDIA_UNMOUNTABLE, Intent.ACTION_MEDIA_UNMOUNTABLE)133         sEnvironmentToBroadcast.put(Environment.MEDIA_UNMOUNTABLE, Intent.ACTION_MEDIA_UNMOUNTABLE);
sEnvironmentToBroadcast.put(Environment.MEDIA_REMOVED, Intent.ACTION_MEDIA_REMOVED)134         sEnvironmentToBroadcast.put(Environment.MEDIA_REMOVED, Intent.ACTION_MEDIA_REMOVED);
sEnvironmentToBroadcast.put(Environment.MEDIA_BAD_REMOVAL, Intent.ACTION_MEDIA_BAD_REMOVAL)135         sEnvironmentToBroadcast.put(Environment.MEDIA_BAD_REMOVAL, Intent.ACTION_MEDIA_BAD_REMOVAL);
136 
sStateToDescrip.put(VolumeInfo.STATE_UNMOUNTED, R.string.ext_media_status_unmounted)137         sStateToDescrip.put(VolumeInfo.STATE_UNMOUNTED, R.string.ext_media_status_unmounted);
sStateToDescrip.put(VolumeInfo.STATE_CHECKING, R.string.ext_media_status_checking)138         sStateToDescrip.put(VolumeInfo.STATE_CHECKING, R.string.ext_media_status_checking);
sStateToDescrip.put(VolumeInfo.STATE_MOUNTED, R.string.ext_media_status_mounted)139         sStateToDescrip.put(VolumeInfo.STATE_MOUNTED, R.string.ext_media_status_mounted);
sStateToDescrip.put(VolumeInfo.STATE_MOUNTED_READ_ONLY, R.string.ext_media_status_mounted_ro)140         sStateToDescrip.put(VolumeInfo.STATE_MOUNTED_READ_ONLY, R.string.ext_media_status_mounted_ro);
sStateToDescrip.put(VolumeInfo.STATE_FORMATTING, R.string.ext_media_status_formatting)141         sStateToDescrip.put(VolumeInfo.STATE_FORMATTING, R.string.ext_media_status_formatting);
sStateToDescrip.put(VolumeInfo.STATE_EJECTING, R.string.ext_media_status_ejecting)142         sStateToDescrip.put(VolumeInfo.STATE_EJECTING, R.string.ext_media_status_ejecting);
sStateToDescrip.put(VolumeInfo.STATE_UNMOUNTABLE, R.string.ext_media_status_unmountable)143         sStateToDescrip.put(VolumeInfo.STATE_UNMOUNTABLE, R.string.ext_media_status_unmountable);
sStateToDescrip.put(VolumeInfo.STATE_REMOVED, R.string.ext_media_status_removed)144         sStateToDescrip.put(VolumeInfo.STATE_REMOVED, R.string.ext_media_status_removed);
sStateToDescrip.put(VolumeInfo.STATE_BAD_REMOVAL, R.string.ext_media_status_bad_removal)145         sStateToDescrip.put(VolumeInfo.STATE_BAD_REMOVAL, R.string.ext_media_status_bad_removal);
146     }
147 
148     /** vold state */
149     public final String id;
150     public final int type;
151     public final DiskInfo disk;
152     public final String partGuid;
153     public int mountFlags = 0;
154     public int mountUserId = -1;
155     public int state = STATE_UNMOUNTED;
156     public String fsType;
157     public String fsUuid;
158     public String fsLabel;
159     public String path;
160     public String internalPath;
161 
VolumeInfo(String id, int type, DiskInfo disk, String partGuid)162     public VolumeInfo(String id, int type, DiskInfo disk, String partGuid) {
163         this.id = Preconditions.checkNotNull(id);
164         this.type = type;
165         this.disk = disk;
166         this.partGuid = partGuid;
167     }
168 
VolumeInfo(Parcel parcel)169     public VolumeInfo(Parcel parcel) {
170         id = parcel.readString();
171         type = parcel.readInt();
172         if (parcel.readInt() != 0) {
173             disk = DiskInfo.CREATOR.createFromParcel(parcel);
174         } else {
175             disk = null;
176         }
177         partGuid = parcel.readString();
178         mountFlags = parcel.readInt();
179         mountUserId = parcel.readInt();
180         state = parcel.readInt();
181         fsType = parcel.readString();
182         fsUuid = parcel.readString();
183         fsLabel = parcel.readString();
184         path = parcel.readString();
185         internalPath = parcel.readString();
186     }
187 
getEnvironmentForState(int state)188     public static @NonNull String getEnvironmentForState(int state) {
189         final String envState = sStateToEnvironment.get(state);
190         if (envState != null) {
191             return envState;
192         } else {
193             return Environment.MEDIA_UNKNOWN;
194         }
195     }
196 
getBroadcastForEnvironment(String envState)197     public static @Nullable String getBroadcastForEnvironment(String envState) {
198         return sEnvironmentToBroadcast.get(envState);
199     }
200 
getBroadcastForState(int state)201     public static @Nullable String getBroadcastForState(int state) {
202         return getBroadcastForEnvironment(getEnvironmentForState(state));
203     }
204 
getDescriptionComparator()205     public static @NonNull Comparator<VolumeInfo> getDescriptionComparator() {
206         return sDescriptionComparator;
207     }
208 
getId()209     public @NonNull String getId() {
210         return id;
211     }
212 
getDisk()213     public @Nullable DiskInfo getDisk() {
214         return disk;
215     }
216 
getDiskId()217     public @Nullable String getDiskId() {
218         return (disk != null) ? disk.id : null;
219     }
220 
getType()221     public int getType() {
222         return type;
223     }
224 
getState()225     public int getState() {
226         return state;
227     }
228 
getStateDescription()229     public int getStateDescription() {
230         return sStateToDescrip.get(state, 0);
231     }
232 
getFsUuid()233     public @Nullable String getFsUuid() {
234         return fsUuid;
235     }
236 
getMountUserId()237     public int getMountUserId() {
238         return mountUserId;
239     }
240 
getDescription()241     public @Nullable String getDescription() {
242         if (ID_PRIVATE_INTERNAL.equals(id) || ID_EMULATED_INTERNAL.equals(id)) {
243             return Resources.getSystem().getString(com.android.internal.R.string.storage_internal);
244         } else if (!TextUtils.isEmpty(fsLabel)) {
245             return fsLabel;
246         } else {
247             return null;
248         }
249     }
250 
isMountedReadable()251     public boolean isMountedReadable() {
252         return state == STATE_MOUNTED || state == STATE_MOUNTED_READ_ONLY;
253     }
254 
isMountedWritable()255     public boolean isMountedWritable() {
256         return state == STATE_MOUNTED;
257     }
258 
isPrimary()259     public boolean isPrimary() {
260         return (mountFlags & MOUNT_FLAG_PRIMARY) != 0;
261     }
262 
isPrimaryPhysical()263     public boolean isPrimaryPhysical() {
264         return isPrimary() && (getType() == TYPE_PUBLIC);
265     }
266 
isVisible()267     public boolean isVisible() {
268         return (mountFlags & MOUNT_FLAG_VISIBLE) != 0;
269     }
270 
isVisibleForRead(int userId)271     public boolean isVisibleForRead(int userId) {
272         if (type == TYPE_PUBLIC) {
273             if (isPrimary() && mountUserId != userId) {
274                 // Primary physical is only visible to single user
275                 return false;
276             } else {
277                 return isVisible();
278             }
279         } else if (type == TYPE_EMULATED) {
280             return isVisible();
281         } else {
282             return false;
283         }
284     }
285 
isVisibleForWrite(int userId)286     public boolean isVisibleForWrite(int userId) {
287         if (type == TYPE_PUBLIC && mountUserId == userId) {
288             return isVisible();
289         } else if (type == TYPE_EMULATED) {
290             return isVisible();
291         } else {
292             return false;
293         }
294     }
295 
getPath()296     public File getPath() {
297         return (path != null) ? new File(path) : null;
298     }
299 
getInternalPath()300     public File getInternalPath() {
301         return (internalPath != null) ? new File(internalPath) : null;
302     }
303 
getPathForUser(int userId)304     public File getPathForUser(int userId) {
305         if (path == null) {
306             return null;
307         } else if (type == TYPE_PUBLIC) {
308             return new File(path);
309         } else if (type == TYPE_EMULATED) {
310             return new File(path, Integer.toString(userId));
311         } else {
312             return null;
313         }
314     }
315 
316     /**
317      * Path which is accessible to apps holding
318      * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE}.
319      */
getInternalPathForUser(int userId)320     public File getInternalPathForUser(int userId) {
321         if (type == TYPE_PUBLIC) {
322             // TODO: plumb through cleaner path from vold
323             return new File(path.replace("/storage/", "/mnt/media_rw/"));
324         } else {
325             return getPathForUser(userId);
326         }
327     }
328 
buildStorageVolume(Context context, int userId, boolean reportUnmounted)329     public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) {
330         final StorageManager storage = context.getSystemService(StorageManager.class);
331 
332         final boolean removable;
333         final boolean emulated;
334         final boolean allowMassStorage = false;
335         final String envState = reportUnmounted
336                 ? Environment.MEDIA_UNMOUNTED : getEnvironmentForState(state);
337 
338         File userPath = getPathForUser(userId);
339         if (userPath == null) {
340             userPath = new File("/dev/null");
341         }
342 
343         String description = null;
344         String derivedFsUuid = fsUuid;
345         long mtpReserveSize = 0;
346         long maxFileSize = 0;
347         int mtpStorageId = StorageVolume.STORAGE_ID_INVALID;
348 
349         if (type == TYPE_EMULATED) {
350             emulated = true;
351 
352             final VolumeInfo privateVol = storage.findPrivateForEmulated(this);
353             if (privateVol != null) {
354                 description = storage.getBestVolumeDescription(privateVol);
355                 derivedFsUuid = privateVol.fsUuid;
356             }
357 
358             if (isPrimary()) {
359                 mtpStorageId = StorageVolume.STORAGE_ID_PRIMARY;
360             }
361 
362             mtpReserveSize = storage.getStorageLowBytes(userPath);
363 
364             if (ID_EMULATED_INTERNAL.equals(id)) {
365                 removable = false;
366             } else {
367                 removable = true;
368             }
369 
370         } else if (type == TYPE_PUBLIC) {
371             emulated = false;
372             removable = true;
373 
374             description = storage.getBestVolumeDescription(this);
375 
376             if (isPrimary()) {
377                 mtpStorageId = StorageVolume.STORAGE_ID_PRIMARY;
378             } else {
379                 // Since MediaProvider currently persists this value, we need a
380                 // value that is stable over time.
381                 mtpStorageId = buildStableMtpStorageId(fsUuid);
382             }
383 
384             if ("vfat".equals(fsType)) {
385                 maxFileSize = 4294967295L;
386             }
387 
388         } else {
389             throw new IllegalStateException("Unexpected volume type " + type);
390         }
391 
392         if (description == null) {
393             description = context.getString(android.R.string.unknownName);
394         }
395 
396         return new StorageVolume(id, mtpStorageId, userPath, description, isPrimary(), removable,
397                 emulated, mtpReserveSize, allowMassStorage, maxFileSize, new UserHandle(userId),
398                 derivedFsUuid, envState);
399     }
400 
buildStableMtpStorageId(String fsUuid)401     public static int buildStableMtpStorageId(String fsUuid) {
402         if (TextUtils.isEmpty(fsUuid)) {
403             return StorageVolume.STORAGE_ID_INVALID;
404         } else {
405             int hash = 0;
406             for (int i = 0; i < fsUuid.length(); ++i) {
407                 hash = 31 * hash + fsUuid.charAt(i);
408             }
409             hash = (hash ^ (hash << 16)) & 0xffff0000;
410             // Work around values that the spec doesn't allow, or that we've
411             // reserved for primary
412             if (hash == 0x00000000) hash = 0x00020000;
413             if (hash == 0x00010000) hash = 0x00020000;
414             if (hash == 0xffff0000) hash = 0xfffe0000;
415             return hash | 0x0001;
416         }
417     }
418 
419     // TODO: avoid this layering violation
420     private static final String DOCUMENT_AUTHORITY = "com.android.externalstorage.documents";
421     private static final String DOCUMENT_ROOT_PRIMARY_EMULATED = "primary";
422 
423     /**
424      * Build an intent to browse the contents of this volume. Only valid for
425      * {@link #TYPE_EMULATED} or {@link #TYPE_PUBLIC}.
426      */
buildBrowseIntent()427     public Intent buildBrowseIntent() {
428         final Uri uri;
429         if (type == VolumeInfo.TYPE_PUBLIC) {
430             uri = DocumentsContract.buildRootUri(DOCUMENT_AUTHORITY, fsUuid);
431         } else if (type == VolumeInfo.TYPE_EMULATED && isPrimary()) {
432             uri = DocumentsContract.buildRootUri(DOCUMENT_AUTHORITY,
433                     DOCUMENT_ROOT_PRIMARY_EMULATED);
434         } else {
435             return null;
436         }
437 
438         final Intent intent = new Intent(DocumentsContract.ACTION_BROWSE_DOCUMENT_ROOT);
439         intent.addCategory(Intent.CATEGORY_DEFAULT);
440         intent.setData(uri);
441         return intent;
442     }
443 
444     @Override
toString()445     public String toString() {
446         final CharArrayWriter writer = new CharArrayWriter();
447         dump(new IndentingPrintWriter(writer, "    ", 80));
448         return writer.toString();
449     }
450 
dump(IndentingPrintWriter pw)451     public void dump(IndentingPrintWriter pw) {
452         pw.println("VolumeInfo{" + id + "}:");
453         pw.increaseIndent();
454         pw.printPair("type", DebugUtils.valueToString(getClass(), "TYPE_", type));
455         pw.printPair("diskId", getDiskId());
456         pw.printPair("partGuid", partGuid);
457         pw.printPair("mountFlags", DebugUtils.flagsToString(getClass(), "MOUNT_FLAG_", mountFlags));
458         pw.printPair("mountUserId", mountUserId);
459         pw.printPair("state", DebugUtils.valueToString(getClass(), "STATE_", state));
460         pw.println();
461         pw.printPair("fsType", fsType);
462         pw.printPair("fsUuid", fsUuid);
463         pw.printPair("fsLabel", fsLabel);
464         pw.println();
465         pw.printPair("path", path);
466         pw.printPair("internalPath", internalPath);
467         pw.decreaseIndent();
468         pw.println();
469     }
470 
471     @Override
clone()472     public VolumeInfo clone() {
473         final Parcel temp = Parcel.obtain();
474         try {
475             writeToParcel(temp, 0);
476             temp.setDataPosition(0);
477             return CREATOR.createFromParcel(temp);
478         } finally {
479             temp.recycle();
480         }
481     }
482 
483     @Override
equals(Object o)484     public boolean equals(Object o) {
485         if (o instanceof VolumeInfo) {
486             return Objects.equals(id, ((VolumeInfo) o).id);
487         } else {
488             return false;
489         }
490     }
491 
492     @Override
hashCode()493     public int hashCode() {
494         return id.hashCode();
495     }
496 
497     public static final Creator<VolumeInfo> CREATOR = new Creator<VolumeInfo>() {
498         @Override
499         public VolumeInfo createFromParcel(Parcel in) {
500             return new VolumeInfo(in);
501         }
502 
503         @Override
504         public VolumeInfo[] newArray(int size) {
505             return new VolumeInfo[size];
506         }
507     };
508 
509     @Override
describeContents()510     public int describeContents() {
511         return 0;
512     }
513 
514     @Override
writeToParcel(Parcel parcel, int flags)515     public void writeToParcel(Parcel parcel, int flags) {
516         parcel.writeString(id);
517         parcel.writeInt(type);
518         if (disk != null) {
519             parcel.writeInt(1);
520             disk.writeToParcel(parcel, flags);
521         } else {
522             parcel.writeInt(0);
523         }
524         parcel.writeString(partGuid);
525         parcel.writeInt(mountFlags);
526         parcel.writeInt(mountUserId);
527         parcel.writeInt(state);
528         parcel.writeString(fsType);
529         parcel.writeString(fsUuid);
530         parcel.writeString(fsLabel);
531         parcel.writeString(path);
532         parcel.writeString(internalPath);
533     }
534 }
535