1 /*
2  * Copyright (C) 2017 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.mtp;
18 
19 import android.media.MediaFile;
20 import android.os.FileObserver;
21 import android.os.SystemProperties;
22 import android.os.storage.StorageVolume;
23 import android.system.ErrnoException;
24 import android.system.Os;
25 import android.system.StructStat;
26 import android.util.Log;
27 
28 import com.android.internal.util.Preconditions;
29 
30 import java.io.IOException;
31 import java.nio.file.DirectoryIteratorException;
32 import java.nio.file.DirectoryStream;
33 import java.nio.file.Files;
34 import java.nio.file.Path;
35 import java.nio.file.Paths;
36 import java.util.ArrayList;
37 import java.util.Collection;
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Set;
42 import java.util.function.Supplier;
43 
44 /**
45  * MtpStorageManager provides functionality for listing, tracking, and notifying MtpServer of
46  * filesystem changes. As directories are listed, this class will cache the results,
47  * and send events when objects are added/removed from cached directories.
48  * {@hide}
49  */
50 public class MtpStorageManager {
51     private static final String TAG = MtpStorageManager.class.getSimpleName();
52     public static boolean sDebug = false;
53 
54     // Inotify flags not provided by FileObserver
55     private static final int IN_ONLYDIR = 0x01000000;
56     private static final int IN_Q_OVERFLOW = 0x00004000;
57     private static final int IN_IGNORED    = 0x00008000;
58     private static final int IN_ISDIR = 0x40000000;
59 
60     private class MtpObjectObserver extends FileObserver {
61         MtpObject mObject;
62 
MtpObjectObserver(MtpObject object)63         MtpObjectObserver(MtpObject object) {
64             super(object.getPath().toString(),
65                     MOVED_FROM | MOVED_TO | DELETE | CREATE | IN_ONLYDIR
66                   | CLOSE_WRITE);
67             mObject = object;
68         }
69 
70         @Override
onEvent(int event, String path)71         public void onEvent(int event, String path) {
72             synchronized (MtpStorageManager.this) {
73                 if ((event & IN_Q_OVERFLOW) != 0) {
74                     // We are out of space in the inotify queue.
75                     Log.e(TAG, "Received Inotify overflow event!");
76                 }
77                 MtpObject obj = mObject.getChild(path);
78                 if ((event & MOVED_TO) != 0 || (event & CREATE) != 0) {
79                     if (sDebug)
80                         Log.i(TAG, "Got inotify added event for " + path + " " + event);
81                     handleAddedObject(mObject, path, (event & IN_ISDIR) != 0);
82                 } else if ((event & MOVED_FROM) != 0 || (event & DELETE) != 0) {
83                     if (obj == null) {
84                         Log.w(TAG, "Object was null in event " + path);
85                         return;
86                     }
87                     if (sDebug)
88                         Log.i(TAG, "Got inotify removed event for " + path + " " + event);
89                     handleRemovedObject(obj);
90                 } else if ((event & IN_IGNORED) != 0) {
91                     if (sDebug)
92                         Log.i(TAG, "inotify for " + mObject.getPath() + " deleted");
93                     if (mObject.mObserver != null)
94                         mObject.mObserver.stopWatching();
95                     mObject.mObserver = null;
96                 } else if ((event & CLOSE_WRITE) != 0) {
97                     if (sDebug)
98                         Log.i(TAG, "inotify for " + mObject.getPath() + " CLOSE_WRITE: " + path);
99                     handleChangedObject(mObject, path);
100                 } else {
101                     Log.w(TAG, "Got unrecognized event " + path + " " + event);
102                 }
103             }
104         }
105 
106         @Override
finalize()107         public void finalize() {
108             // If the server shuts down and starts up again, the new server's observers can be
109             // invalidated by the finalize() calls of the previous server's observers.
110             // Hence, disable the automatic stopWatching() call in FileObserver#finalize, and
111             // always call stopWatching() manually whenever an observer should be shut down.
112         }
113     }
114 
115     /**
116      * Describes how the object is being acted on, to determine how events are handled.
117      */
118     private enum MtpObjectState {
119         NORMAL,
120         FROZEN,             // Object is going to be modified in this session.
121         FROZEN_ADDED,       // Object was frozen, and has been added.
122         FROZEN_REMOVED,     // Object was frozen, and has been removed.
123         FROZEN_ONESHOT_ADD, // Object is waiting for single add event before being unfrozen.
124         FROZEN_ONESHOT_DEL, // Object is waiting for single remove event and will then be removed.
125     }
126 
127     /**
128      * Describes the current operation being done on an object. Determines whether observers are
129      * created on new folders.
130      */
131     private enum MtpOperation {
132         NONE,     // Any new folders not added as part of the session are immediately observed.
133         ADD,      // New folders added as part of the session are immediately observed.
134         RENAME,   // Renamed or moved folders are not immediately observed.
135         COPY,     // Copied folders are immediately observed iff the original was.
136         DELETE,   // Exists for debugging purposes only.
137     }
138 
139     /** MtpObject represents either a file or directory in an associated storage. **/
140     public static class MtpObject {
141         private MtpStorage mStorage;
142         // null for root objects
143         private MtpObject mParent;
144 
145         private String mName;
146         private int mId;
147         private MtpObjectState mState;
148         private MtpOperation mOp;
149 
150         private boolean mVisited;
151         private boolean mIsDir;
152 
153         // null if not a directory
154         private HashMap<String, MtpObject> mChildren;
155         // null if not both a directory and visited
156         private FileObserver mObserver;
157 
MtpObject(String name, int id, MtpStorage storage, MtpObject parent, boolean isDir)158         MtpObject(String name, int id, MtpStorage storage, MtpObject parent, boolean isDir) {
159             mId = id;
160             mName = name;
161             mStorage = Preconditions.checkNotNull(storage);
162             mParent = parent;
163             mObserver = null;
164             mVisited = false;
165             mState = MtpObjectState.NORMAL;
166             mIsDir = isDir;
167             mOp = MtpOperation.NONE;
168 
169             mChildren = mIsDir ? new HashMap<>() : null;
170         }
171 
172         /** Public methods for getting object info **/
173 
getName()174         public String getName() {
175             return mName;
176         }
177 
getId()178         public int getId() {
179             return mId;
180         }
181 
isDir()182         public boolean isDir() {
183             return mIsDir;
184         }
185 
getFormat()186         public int getFormat() {
187             return mIsDir ? MtpConstants.FORMAT_ASSOCIATION : MediaFile.getFormatCode(mName, null);
188         }
189 
getStorageId()190         public int getStorageId() {
191             return getRoot().getId();
192         }
193 
getModifiedTime()194         public long getModifiedTime() {
195             return getPath().toFile().lastModified() / 1000;
196         }
197 
getParent()198         public MtpObject getParent() {
199             return mParent;
200         }
201 
getRoot()202         public MtpObject getRoot() {
203             return isRoot() ? this : mParent.getRoot();
204         }
205 
getSize()206         public long getSize() {
207             return mIsDir ? 0 : maybeApplyTranscodeLengthWorkaround(getPath().toFile().length());
208         }
209 
maybeApplyTranscodeLengthWorkaround(long length)210         private long maybeApplyTranscodeLengthWorkaround(long length) {
211             // Windows truncates transferred files to the size advertised in the object property.
212             if (mStorage.isHostWindows() && isTranscodeMtpEnabled() && isFileTranscodeSupported()) {
213                 // If the file supports transcoding, we double the returned size to accommodate
214                 // the increase in size from transcoding to AVC. This is the same heuristic
215                 // applied in the FUSE daemon (MediaProvider).
216                 return length * 2;
217             }
218             return length;
219         }
220 
isTranscodeMtpEnabled()221         private boolean isTranscodeMtpEnabled() {
222             return SystemProperties.getBoolean("sys.fuse.transcode_mtp", false);
223         }
224 
isFileTranscodeSupported()225         private boolean isFileTranscodeSupported() {
226             // Check if the file supports transcoding by reading the |st_nlinks| struct stat
227             // field. This will be > 1 if the file supports transcoding. The FUSE daemon
228             // sets the field accordingly to enable the MTP stack workaround some Windows OS
229             // MTP client bug where they ignore the size returned as part of getting the MTP
230             // object, see MtpServer#doGetObject.
231             final Path path = getPath();
232             try {
233                 StructStat stat = Os.stat(path.toString());
234                 return stat.st_nlink > 1;
235             } catch (ErrnoException e) {
236                 Log.w(TAG, "Failed to stat path: " + getPath() + ". Ignoring transcoding.");
237                 return false;
238             }
239         }
240 
getPath()241         public Path getPath() {
242             return isRoot() ? Paths.get(mName) : mParent.getPath().resolve(mName);
243         }
244 
isRoot()245         public boolean isRoot() {
246             return mParent == null;
247         }
248 
getVolumeName()249         public String getVolumeName() {
250             return mStorage.getVolumeName();
251         }
252 
253         /** For MtpStorageManager only **/
254 
setName(String name)255         private void setName(String name) {
256             mName = name;
257         }
258 
setId(int id)259         private void setId(int id) {
260             mId = id;
261         }
262 
isVisited()263         private boolean isVisited() {
264             return mVisited;
265         }
266 
setParent(MtpObject parent)267         private void setParent(MtpObject parent) {
268             if (this.getStorageId() != parent.getStorageId()) {
269                 mStorage = Preconditions.checkNotNull(parent.getStorage());
270             }
271             mParent = parent;
272         }
273 
getStorage()274         private MtpStorage getStorage() {
275             return mStorage;
276         }
277 
setDir(boolean dir)278         private void setDir(boolean dir) {
279             if (dir != mIsDir) {
280                 mIsDir = dir;
281                 mChildren = mIsDir ? new HashMap<>() : null;
282             }
283         }
284 
setVisited(boolean visited)285         private void setVisited(boolean visited) {
286             mVisited = visited;
287         }
288 
getState()289         private MtpObjectState getState() {
290             return mState;
291         }
292 
setState(MtpObjectState state)293         private void setState(MtpObjectState state) {
294             mState = state;
295             if (mState == MtpObjectState.NORMAL)
296                 mOp = MtpOperation.NONE;
297         }
298 
getOperation()299         private MtpOperation getOperation() {
300             return mOp;
301         }
302 
setOperation(MtpOperation op)303         private void setOperation(MtpOperation op) {
304             mOp = op;
305         }
306 
getObserver()307         private FileObserver getObserver() {
308             return mObserver;
309         }
310 
setObserver(FileObserver observer)311         private void setObserver(FileObserver observer) {
312             mObserver = observer;
313         }
314 
addChild(MtpObject child)315         private void addChild(MtpObject child) {
316             mChildren.put(child.getName(), child);
317         }
318 
getChild(String name)319         private MtpObject getChild(String name) {
320             return mChildren.get(name);
321         }
322 
getChildren()323         private Collection<MtpObject> getChildren() {
324             return mChildren.values();
325         }
326 
exists()327         private boolean exists() {
328             return getPath().toFile().exists();
329         }
330 
copy(boolean recursive)331         private MtpObject copy(boolean recursive) {
332             MtpObject copy = new MtpObject(mName, mId, mStorage, mParent, mIsDir);
333             copy.mIsDir = mIsDir;
334             copy.mVisited = mVisited;
335             copy.mState = mState;
336             copy.mChildren = mIsDir ? new HashMap<>() : null;
337             if (recursive && mIsDir) {
338                 for (MtpObject child : mChildren.values()) {
339                     MtpObject childCopy = child.copy(true);
340                     childCopy.setParent(copy);
341                     copy.addChild(childCopy);
342                 }
343             }
344             return copy;
345         }
346     }
347 
348     /**
349      * A class that processes generated filesystem events.
350      */
351     public static abstract class MtpNotifier {
352         /**
353          * Called when an object is added.
354          */
sendObjectAdded(int id)355         public abstract void sendObjectAdded(int id);
356 
357         /**
358          * Called when an object is deleted.
359          */
sendObjectRemoved(int id)360         public abstract void sendObjectRemoved(int id);
361 
362         /**
363          * Called when an object info is changed.
364          */
sendObjectInfoChanged(int id)365         public abstract void sendObjectInfoChanged(int id);
366     }
367 
368     private MtpNotifier mMtpNotifier;
369 
370     // A cache of MtpObjects. The objects in the cache are keyed by object id.
371     // The root object of each storage isn't in this map since they all have ObjectId 0.
372     // Instead, they can be found in mRoots keyed by storageId.
373     private HashMap<Integer, MtpObject> mObjects;
374 
375     // A cache of the root MtpObject for each storage, keyed by storage id.
376     private HashMap<Integer, MtpObject> mRoots;
377 
378     // Object and Storage ids are allocated incrementally and not to be reused.
379     private int mNextObjectId;
380     private int mNextStorageId;
381 
382     // Special subdirectories. When set, only return objects rooted in these directories, and do
383     // not allow them to be modified.
384     private Set<String> mSubdirectories;
385 
386     private volatile boolean mCheckConsistency;
387     private Thread mConsistencyThread;
388 
MtpStorageManager(MtpNotifier notifier, Set<String> subdirectories)389     public MtpStorageManager(MtpNotifier notifier, Set<String> subdirectories) {
390         mMtpNotifier = notifier;
391         mSubdirectories = subdirectories;
392         mObjects = new HashMap<>();
393         mRoots = new HashMap<>();
394         mNextObjectId = 1;
395         mNextStorageId = 1;
396 
397         mCheckConsistency = false; // Set to true to turn on automatic consistency checking
398         mConsistencyThread = new Thread(() -> {
399             while (mCheckConsistency) {
400                 try {
401                     Thread.sleep(15 * 1000);
402                 } catch (InterruptedException e) {
403                     return;
404                 }
405                 if (MtpStorageManager.this.checkConsistency()) {
406                     Log.v(TAG, "Cache is consistent");
407                 } else {
408                     Log.w(TAG, "Cache is not consistent");
409                 }
410             }
411         });
412         if (mCheckConsistency)
413             mConsistencyThread.start();
414     }
415 
416     /**
417      * Clean up resources used by the storage manager.
418      */
close()419     public synchronized void close() {
420         for (MtpObject obj : mObjects.values()) {
421             if (obj.getObserver() != null) {
422                 obj.getObserver().stopWatching();
423                 obj.setObserver(null);
424             }
425         }
426         for (MtpObject obj : mRoots.values()) {
427             if (obj.getObserver() != null) {
428                 obj.getObserver().stopWatching();
429                 obj.setObserver(null);
430             }
431         }
432 
433         // Shut down the consistency checking thread
434         if (mCheckConsistency) {
435             mCheckConsistency = false;
436             mConsistencyThread.interrupt();
437             try {
438                 mConsistencyThread.join();
439             } catch (InterruptedException e) {
440                 // ignore
441             }
442         }
443     }
444 
445     /**
446      * Sets the special subdirectories, which are the subdirectories of root storage that queries
447      * are restricted to. Must be done before any root storages are accessed.
448      * @param subDirs Subdirectories to set, or null to reset.
449      */
setSubdirectories(Set<String> subDirs)450     public synchronized void setSubdirectories(Set<String> subDirs) {
451         mSubdirectories = subDirs;
452     }
453 
454     /**
455      * Allocates an MTP storage id for the given volume and add it to current roots.
456      * @param volume Storage to add.
457      * @return the associated MtpStorage
458      */
addMtpStorage(StorageVolume volume, Supplier<Boolean> isHostWindows)459     public synchronized MtpStorage addMtpStorage(StorageVolume volume,
460                                                  Supplier<Boolean> isHostWindows) {
461         int storageId = ((getNextStorageId() & 0x0000FFFF) << 16) + 1;
462         MtpStorage storage = new MtpStorage(volume, storageId, isHostWindows);
463         MtpObject root = new MtpObject(storage.getPath(), storageId, storage, /* parent= */ null,
464                                        /* isDir= */ true);
465         mRoots.put(storageId, root);
466         return storage;
467     }
468 
469     /**
470      * Removes the given storage and all associated items from the cache.
471      * @param storage Storage to remove.
472      */
removeMtpStorage(MtpStorage storage)473     public synchronized void removeMtpStorage(MtpStorage storage) {
474         removeObjectFromCache(getStorageRoot(storage.getStorageId()), true, true);
475     }
476 
477     /**
478      * Checks if the given object can be renamed, moved, or deleted.
479      * If there are special subdirectories, they cannot be modified.
480      * @param obj Object to check.
481      * @return Whether object can be modified.
482      */
isSpecialSubDir(MtpObject obj)483     private synchronized boolean isSpecialSubDir(MtpObject obj) {
484         return obj.getParent().isRoot() && mSubdirectories != null
485                 && !mSubdirectories.contains(obj.getName());
486     }
487 
488     /**
489      * Get the object with the specified path. Visit any necessary directories on the way.
490      * @param path Full path of the object to find.
491      * @return The desired object, or null if it cannot be found.
492      */
getByPath(String path)493     public synchronized MtpObject getByPath(String path) {
494         MtpObject obj = null;
495         for (MtpObject root : mRoots.values()) {
496             if (path.startsWith(root.getName())) {
497                 obj = root;
498                 path = path.substring(root.getName().length());
499             }
500         }
501         for (String name : path.split("/")) {
502             if (obj == null || !obj.isDir())
503                 return null;
504             if ("".equals(name))
505                 continue;
506             if (!obj.isVisited())
507                 getChildren(obj);
508             obj = obj.getChild(name);
509         }
510         return obj;
511     }
512 
513     /**
514      * Get the object with specified id.
515      * @param id Id of object. must not be 0 or 0xFFFFFFFF
516      * @return Object, or null if error.
517      */
getObject(int id)518     public synchronized MtpObject getObject(int id) {
519         if (id == 0 || id == 0xFFFFFFFF) {
520             Log.w(TAG, "Can't get root storages with getObject()");
521             return null;
522         }
523         if (!mObjects.containsKey(id)) {
524             Log.w(TAG, "Id " + id + " doesn't exist");
525             return null;
526         }
527         return mObjects.get(id);
528     }
529 
530     /**
531      * Get the storage with specified id.
532      * @param id Storage id.
533      * @return Object that is the root of the storage, or null if error.
534      */
getStorageRoot(int id)535     public MtpObject getStorageRoot(int id) {
536         if (!mRoots.containsKey(id)) {
537             Log.w(TAG, "StorageId " + id + " doesn't exist");
538             return null;
539         }
540         return mRoots.get(id);
541     }
542 
getNextObjectId()543     private int getNextObjectId() {
544         int ret = mNextObjectId;
545         // Treat the id as unsigned int
546         mNextObjectId = (int) ((long) mNextObjectId + 1);
547         return ret;
548     }
549 
getNextStorageId()550     private int getNextStorageId() {
551         return mNextStorageId++;
552     }
553 
554     /**
555      * Get all objects matching the given parent, format, and storage
556      * @param parent object id of the parent. 0 for all objects, 0xFFFFFFFF for all object in root
557      * @param format format of returned objects. 0 for any format
558      * @param storageId storage id to look in. 0xFFFFFFFF for all storages
559      * @return A list of matched objects, or null if error
560      */
getObjects(int parent, int format, int storageId)561     public synchronized List<MtpObject> getObjects(int parent, int format, int storageId) {
562         boolean recursive = parent == 0;
563         ArrayList<MtpObject> objs = new ArrayList<>();
564         boolean ret = true;
565         if (parent == 0xFFFFFFFF)
566             parent = 0;
567         if (storageId == 0xFFFFFFFF) {
568             // query all stores
569             if (parent == 0) {
570                 // Get the objects of this format and parent in each store.
571                 for (MtpObject root : mRoots.values()) {
572                     ret &= getObjects(objs, root, format, recursive);
573                 }
574                 return ret ? objs : null;
575             }
576         }
577         MtpObject obj = parent == 0 ? getStorageRoot(storageId) : getObject(parent);
578         if (obj == null)
579             return null;
580         ret = getObjects(objs, obj, format, recursive);
581         return ret ? objs : null;
582     }
583 
getObjects(List<MtpObject> toAdd, MtpObject parent, int format, boolean rec)584     private synchronized boolean getObjects(List<MtpObject> toAdd, MtpObject parent, int format, boolean rec) {
585         Collection<MtpObject> children = getChildren(parent);
586         if (children == null)
587             return false;
588 
589         for (MtpObject o : children) {
590             if (format == 0 || o.getFormat() == format) {
591                 toAdd.add(o);
592             }
593         }
594         boolean ret = true;
595         if (rec) {
596             // Get all objects recursively.
597             for (MtpObject o : children) {
598                 if (o.isDir())
599                     ret &= getObjects(toAdd, o, format, true);
600             }
601         }
602         return ret;
603     }
604 
605     /**
606      * Return the children of the given object. If the object hasn't been visited yet, add
607      * its children to the cache and start observing it.
608      * @param object the parent object
609      * @return The collection of child objects or null if error
610      */
getChildren(MtpObject object)611     private synchronized Collection<MtpObject> getChildren(MtpObject object) {
612         if (object == null || !object.isDir()) {
613             Log.w(TAG, "Can't find children of " + (object == null ? "null" : object.getId()));
614             return null;
615         }
616         if (!object.isVisited()) {
617             Path dir = object.getPath();
618             /*
619              * If a file is added after the observer starts watching the directory, but before
620              * the contents are listed, it will generate an event that will get processed
621              * after this synchronized function returns. We handle this by ignoring object
622              * added events if an object at that path already exists.
623              */
624             if (object.getObserver() != null)
625                 Log.e(TAG, "Observer is not null!");
626             object.setObserver(new MtpObjectObserver(object));
627             object.getObserver().startWatching();
628             try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
629                 for (Path file : stream) {
630                     addObjectToCache(object, file.getFileName().toString(),
631                             file.toFile().isDirectory());
632                 }
633             } catch (IOException | DirectoryIteratorException e) {
634                 Log.e(TAG, e.toString());
635                 object.getObserver().stopWatching();
636                 object.setObserver(null);
637                 return null;
638             }
639             object.setVisited(true);
640         }
641         return object.getChildren();
642     }
643 
644     /**
645      * Create a new object from the given path and add it to the cache.
646      * @param parent The parent object
647      * @param newName Path of the new object
648      * @return the new object if success, else null
649      */
addObjectToCache(MtpObject parent, String newName, boolean isDir)650     private synchronized MtpObject addObjectToCache(MtpObject parent, String newName,
651             boolean isDir) {
652         if (!parent.isRoot() && getObject(parent.getId()) != parent)
653             // parent object has been removed
654             return null;
655         if (parent.getChild(newName) != null) {
656             // Object already exists
657             return null;
658         }
659         if (mSubdirectories != null && parent.isRoot() && !mSubdirectories.contains(newName)) {
660             // Not one of the restricted subdirectories.
661             return null;
662         }
663 
664         MtpObject obj = new MtpObject(newName, getNextObjectId(), parent.mStorage, parent, isDir);
665         mObjects.put(obj.getId(), obj);
666         parent.addChild(obj);
667         return obj;
668     }
669 
670     /**
671      * Remove the given path from the cache.
672      * @param removed The removed object
673      * @param removeGlobal Whether to remove the object from the global id map
674      * @param recursive Whether to also remove its children recursively.
675      * @return true if successfully removed
676      */
removeObjectFromCache(MtpObject removed, boolean removeGlobal, boolean recursive)677     private synchronized boolean removeObjectFromCache(MtpObject removed, boolean removeGlobal,
678             boolean recursive) {
679         boolean ret = removed.isRoot()
680                 || removed.getParent().mChildren.remove(removed.getName(), removed);
681         if (!ret && sDebug)
682             Log.w(TAG, "Failed to remove from parent " + removed.getPath());
683         if (removed.isRoot()) {
684             ret = mRoots.remove(removed.getId(), removed) && ret;
685         } else if (removeGlobal) {
686             ret = mObjects.remove(removed.getId(), removed) && ret;
687         }
688         if (!ret && sDebug)
689             Log.w(TAG, "Failed to remove from global cache " + removed.getPath());
690         if (removed.getObserver() != null) {
691             removed.getObserver().stopWatching();
692             removed.setObserver(null);
693         }
694         if (removed.isDir() && recursive) {
695             // Remove all descendants from cache recursively
696             Collection<MtpObject> children = new ArrayList<>(removed.getChildren());
697             for (MtpObject child : children) {
698                 ret = removeObjectFromCache(child, removeGlobal, true) && ret;
699             }
700         }
701         return ret;
702     }
703 
handleAddedObject(MtpObject parent, String path, boolean isDir)704     private synchronized void handleAddedObject(MtpObject parent, String path, boolean isDir) {
705         MtpOperation op = MtpOperation.NONE;
706         MtpObject obj = parent.getChild(path);
707         if (obj != null) {
708             MtpObjectState state = obj.getState();
709             op = obj.getOperation();
710             if (obj.isDir() != isDir && state != MtpObjectState.FROZEN_REMOVED)
711                 Log.d(TAG, "Inconsistent directory info! " + obj.getPath());
712             obj.setDir(isDir);
713             switch (state) {
714                 case FROZEN:
715                 case FROZEN_REMOVED:
716                     obj.setState(MtpObjectState.FROZEN_ADDED);
717                     break;
718                 case FROZEN_ONESHOT_ADD:
719                     obj.setState(MtpObjectState.NORMAL);
720                     break;
721                 case NORMAL:
722                 case FROZEN_ADDED:
723                     // This can happen when handling listed object in a new directory.
724                     return;
725                 default:
726                     Log.w(TAG, "Unexpected state in add " + path + " " + state);
727             }
728             if (sDebug)
729                 Log.i(TAG, state + " transitioned to " + obj.getState() + " in op " + op);
730         } else {
731             obj = MtpStorageManager.this.addObjectToCache(parent, path, isDir);
732             if (obj != null) {
733                 MtpStorageManager.this.mMtpNotifier.sendObjectAdded(obj.getId());
734             } else {
735                 if (sDebug)
736                     Log.w(TAG, "object " + path + " already exists");
737                 return;
738             }
739         }
740         if (isDir) {
741             // If this was added as part of a rename do not visit or send events.
742             if (op == MtpOperation.RENAME)
743                 return;
744 
745             // If it was part of a copy operation, then only add observer if it was visited before.
746             if (op == MtpOperation.COPY && !obj.isVisited())
747                 return;
748 
749             if (obj.getObserver() != null) {
750                 Log.e(TAG, "Observer is not null!");
751                 return;
752             }
753             obj.setObserver(new MtpObjectObserver(obj));
754             obj.getObserver().startWatching();
755             obj.setVisited(true);
756 
757             // It's possible that objects were added to a watched directory before the watch can be
758             // created, so manually handle those.
759             try (DirectoryStream<Path> stream = Files.newDirectoryStream(obj.getPath())) {
760                 for (Path file : stream) {
761                     if (sDebug)
762                         Log.i(TAG, "Manually handling event for " + file.getFileName().toString());
763                     handleAddedObject(obj, file.getFileName().toString(),
764                             file.toFile().isDirectory());
765                 }
766             } catch (IOException | DirectoryIteratorException e) {
767                 Log.e(TAG, e.toString());
768                 obj.getObserver().stopWatching();
769                 obj.setObserver(null);
770             }
771         }
772     }
773 
handleRemovedObject(MtpObject obj)774     private synchronized void handleRemovedObject(MtpObject obj) {
775         MtpObjectState state = obj.getState();
776         MtpOperation op = obj.getOperation();
777         switch (state) {
778             case FROZEN_ADDED:
779                 obj.setState(MtpObjectState.FROZEN_REMOVED);
780                 break;
781             case FROZEN_ONESHOT_DEL:
782                 removeObjectFromCache(obj, op != MtpOperation.RENAME, false);
783                 break;
784             case FROZEN:
785                 obj.setState(MtpObjectState.FROZEN_REMOVED);
786                 break;
787             case NORMAL:
788                 if (MtpStorageManager.this.removeObjectFromCache(obj, true, true))
789                     MtpStorageManager.this.mMtpNotifier.sendObjectRemoved(obj.getId());
790                 break;
791             default:
792                 // This shouldn't happen; states correspond to objects that don't exist
793                 Log.e(TAG, "Got unexpected object remove for " + obj.getName());
794         }
795         if (sDebug)
796             Log.i(TAG, state + " transitioned to " + obj.getState() + " in op " + op);
797     }
798 
handleChangedObject(MtpObject parent, String path)799     private synchronized void handleChangedObject(MtpObject parent, String path) {
800         MtpOperation op = MtpOperation.NONE;
801         MtpObject obj = parent.getChild(path);
802         if (obj != null) {
803             // Only handle files for size change notification event
804             if ((!obj.isDir()) && (obj.getSize() > 0))
805             {
806                 MtpObjectState state = obj.getState();
807                 op = obj.getOperation();
808                 MtpStorageManager.this.mMtpNotifier.sendObjectInfoChanged(obj.getId());
809                 if (sDebug)
810                     Log.d(TAG, "sendObjectInfoChanged: id=" + obj.getId() + ",size=" + obj.getSize());
811             }
812         } else {
813             if (sDebug)
814                 Log.w(TAG, "object " + path + " null");
815         }
816     }
817 
818     /**
819      * Block the caller until all events currently in the event queue have been
820      * read and processed. Used for testing purposes.
821      */
flushEvents()822     public void flushEvents() {
823         try {
824             // TODO make this smarter
825             Thread.sleep(500);
826         } catch (InterruptedException e) {
827 
828         }
829     }
830 
831     /**
832      * Dumps a representation of the cache to log.
833      */
dump()834     public synchronized void dump() {
835         for (int key : mObjects.keySet()) {
836             MtpObject obj = mObjects.get(key);
837             Log.i(TAG, key + " | " + (obj.getParent() == null ? obj.getParent().getId() : "null")
838                     + " | " + obj.getName() + " | " + (obj.isDir() ? "dir" : "obj")
839                     + " | " + (obj.isVisited() ? "v" : "nv") + " | " + obj.getState());
840         }
841     }
842 
843     /**
844      * Checks consistency of the cache. This checks whether all objects have correct links
845      * to their parent, and whether directories are missing or have extraneous objects.
846      * @return true iff cache is consistent
847      */
checkConsistency()848     public synchronized boolean checkConsistency() {
849         List<MtpObject> objs = new ArrayList<>();
850         objs.addAll(mRoots.values());
851         objs.addAll(mObjects.values());
852         boolean ret = true;
853         for (MtpObject obj : objs) {
854             if (!obj.exists()) {
855                 Log.w(TAG, "Object doesn't exist " + obj.getPath() + " " + obj.getId());
856                 ret = false;
857             }
858             if (obj.getState() != MtpObjectState.NORMAL) {
859                 Log.w(TAG, "Object " + obj.getPath() + " in state " + obj.getState());
860                 ret = false;
861             }
862             if (obj.getOperation() != MtpOperation.NONE) {
863                 Log.w(TAG, "Object " + obj.getPath() + " in operation " + obj.getOperation());
864                 ret = false;
865             }
866             if (!obj.isRoot() && mObjects.get(obj.getId()) != obj) {
867                 Log.w(TAG, "Object " + obj.getPath() + " is not in map correctly");
868                 ret = false;
869             }
870             if (obj.getParent() != null) {
871                 if (obj.getParent().isRoot() && obj.getParent()
872                         != mRoots.get(obj.getParent().getId())) {
873                     Log.w(TAG, "Root parent is not in root mapping " + obj.getPath());
874                     ret = false;
875                 }
876                 if (!obj.getParent().isRoot() && obj.getParent()
877                         != mObjects.get(obj.getParent().getId())) {
878                     Log.w(TAG, "Parent is not in object mapping " + obj.getPath());
879                     ret = false;
880                 }
881                 if (obj.getParent().getChild(obj.getName()) != obj) {
882                     Log.w(TAG, "Child does not exist in parent " + obj.getPath());
883                     ret = false;
884                 }
885             }
886             if (obj.isDir()) {
887                 if (obj.isVisited() == (obj.getObserver() == null)) {
888                     Log.w(TAG, obj.getPath() + " is " + (obj.isVisited() ? "" : "not ")
889                             + " visited but observer is " + obj.getObserver());
890                     ret = false;
891                 }
892                 if (!obj.isVisited() && obj.getChildren().size() > 0) {
893                     Log.w(TAG, obj.getPath() + " is not visited but has children");
894                     ret = false;
895                 }
896                 try (DirectoryStream<Path> stream = Files.newDirectoryStream(obj.getPath())) {
897                     Set<String> files = new HashSet<>();
898                     for (Path file : stream) {
899                         if (obj.isVisited() &&
900                                 obj.getChild(file.getFileName().toString()) == null &&
901                                 (mSubdirectories == null || !obj.isRoot() ||
902                                         mSubdirectories.contains(file.getFileName().toString()))) {
903                             Log.w(TAG, "File exists in fs but not in children " + file);
904                             ret = false;
905                         }
906                         files.add(file.toString());
907                     }
908                     for (MtpObject child : obj.getChildren()) {
909                         if (!files.contains(child.getPath().toString())) {
910                             Log.w(TAG, "File in children doesn't exist in fs " + child.getPath());
911                             ret = false;
912                         }
913                         if (child != mObjects.get(child.getId())) {
914                             Log.w(TAG, "Child is not in object map " + child.getPath());
915                             ret = false;
916                         }
917                     }
918                 } catch (IOException | DirectoryIteratorException e) {
919                     Log.w(TAG, e.toString());
920                     ret = false;
921                 }
922             }
923         }
924         return ret;
925     }
926 
927     /**
928      * Informs MtpStorageManager that an object with the given path is about to be added.
929      * @param parent The parent object of the object to be added.
930      * @param name Filename of object to add.
931      * @return Object id of the added object, or -1 if it cannot be added.
932      */
beginSendObject(MtpObject parent, String name, int format)933     public synchronized int beginSendObject(MtpObject parent, String name, int format) {
934         if (sDebug)
935             Log.v(TAG, "beginSendObject " + name);
936         if (!parent.isDir())
937             return -1;
938         if (parent.isRoot() && mSubdirectories != null && !mSubdirectories.contains(name))
939             return -1;
940         getChildren(parent); // Ensure parent is visited
941         MtpObject obj  = addObjectToCache(parent, name, format == MtpConstants.FORMAT_ASSOCIATION);
942         if (obj == null)
943             return -1;
944         obj.setState(MtpObjectState.FROZEN);
945         obj.setOperation(MtpOperation.ADD);
946         return obj.getId();
947     }
948 
949     /**
950      * Clean up the object state after a sendObject operation.
951      * @param obj The object, returned from beginAddObject().
952      * @param succeeded Whether the file was successfully created.
953      * @return Whether cache state was successfully cleaned up.
954      */
endSendObject(MtpObject obj, boolean succeeded)955     public synchronized boolean endSendObject(MtpObject obj, boolean succeeded) {
956         if (sDebug)
957             Log.v(TAG, "endSendObject " + succeeded);
958         return generalEndAddObject(obj, succeeded, true);
959     }
960 
961     /**
962      * Informs MtpStorageManager that the given object is about to be renamed.
963      * If this returns true, it must be followed with an endRenameObject()
964      * @param obj Object to be renamed.
965      * @param newName New name of the object.
966      * @return Whether renaming is allowed.
967      */
beginRenameObject(MtpObject obj, String newName)968     public synchronized boolean beginRenameObject(MtpObject obj, String newName) {
969         if (sDebug)
970             Log.v(TAG, "beginRenameObject " + obj.getName() + " " + newName);
971         if (obj.isRoot())
972             return false;
973         if (isSpecialSubDir(obj))
974             return false;
975         if (obj.getParent().getChild(newName) != null)
976             // Object already exists in parent with that name.
977             return false;
978 
979         MtpObject oldObj = obj.copy(false);
980         obj.setName(newName);
981         obj.getParent().addChild(obj);
982         oldObj.getParent().addChild(oldObj);
983         return generalBeginRenameObject(oldObj, obj);
984     }
985 
986     /**
987      * Cleans up cache state after a rename operation and sends any events that were missed.
988      * @param obj The object being renamed, the same one that was passed in beginRenameObject().
989      * @param oldName The previous name of the object.
990      * @param success Whether the rename operation succeeded.
991      * @return Whether state was successfully cleaned up.
992      */
endRenameObject(MtpObject obj, String oldName, boolean success)993     public synchronized boolean endRenameObject(MtpObject obj, String oldName, boolean success) {
994         if (sDebug)
995             Log.v(TAG, "endRenameObject " + success);
996         MtpObject parent = obj.getParent();
997         MtpObject oldObj = parent.getChild(oldName);
998         if (!success) {
999             // If the rename failed, we want oldObj to be the original and obj to be the stand-in.
1000             // Switch the objects, except for their name and state.
1001             MtpObject temp = oldObj;
1002             MtpObjectState oldState = oldObj.getState();
1003             temp.setName(obj.getName());
1004             temp.setState(obj.getState());
1005             oldObj = obj;
1006             oldObj.setName(oldName);
1007             oldObj.setState(oldState);
1008             obj = temp;
1009             parent.addChild(obj);
1010             parent.addChild(oldObj);
1011         }
1012         return generalEndRenameObject(oldObj, obj, success);
1013     }
1014 
1015     /**
1016      * Informs MtpStorageManager that the given object is about to be deleted by the initiator,
1017      * so don't send an event.
1018      * @param obj Object to be deleted.
1019      * @return Whether cache deletion is allowed.
1020      */
beginRemoveObject(MtpObject obj)1021     public synchronized boolean beginRemoveObject(MtpObject obj) {
1022         if (sDebug)
1023             Log.v(TAG, "beginRemoveObject " + obj.getName());
1024         return !obj.isRoot() && !isSpecialSubDir(obj)
1025                 && generalBeginRemoveObject(obj, MtpOperation.DELETE);
1026     }
1027 
1028     /**
1029      * Clean up cache state after a delete operation and send any events that were missed.
1030      * @param obj Object to be deleted, same one passed in beginRemoveObject().
1031      * @param success Whether operation was completed successfully.
1032      * @return Whether cache state is correct.
1033      */
endRemoveObject(MtpObject obj, boolean success)1034     public synchronized boolean endRemoveObject(MtpObject obj, boolean success) {
1035         if (sDebug)
1036             Log.v(TAG, "endRemoveObject " + success);
1037         boolean ret = true;
1038         if (obj.isDir()) {
1039             for (MtpObject child : new ArrayList<>(obj.getChildren()))
1040                 if (child.getOperation() == MtpOperation.DELETE)
1041                     ret = endRemoveObject(child, success) && ret;
1042         }
1043         return generalEndRemoveObject(obj, success, true) && ret;
1044     }
1045 
1046     /**
1047      * Informs MtpStorageManager that the given object is about to be moved to a new parent.
1048      * @param obj Object to be moved.
1049      * @param newParent The new parent object.
1050      * @return Whether the move is allowed.
1051      */
beginMoveObject(MtpObject obj, MtpObject newParent)1052     public synchronized boolean beginMoveObject(MtpObject obj, MtpObject newParent) {
1053         if (sDebug)
1054             Log.v(TAG, "beginMoveObject " + newParent.getPath());
1055         if (obj.isRoot())
1056             return false;
1057         if (isSpecialSubDir(obj))
1058             return false;
1059         getChildren(newParent); // Ensure parent is visited
1060         if (newParent.getChild(obj.getName()) != null)
1061             // Object already exists in parent with that name.
1062             return false;
1063         if (obj.getStorageId() != newParent.getStorageId()) {
1064             /*
1065              * The move is occurring across storages. The observers will not remain functional
1066              * after the move, and the move will not be atomic. We have to copy the file tree
1067              * to the destination and recreate the observers once copy is complete.
1068              */
1069             MtpObject newObj = obj.copy(true);
1070             newObj.setParent(newParent);
1071             newParent.addChild(newObj);
1072             return generalBeginRemoveObject(obj, MtpOperation.RENAME)
1073                     && generalBeginCopyObject(newObj, false);
1074         }
1075         // Move obj to new parent, create a fake object in the old parent.
1076         MtpObject oldObj = obj.copy(false);
1077         obj.setParent(newParent);
1078         oldObj.getParent().addChild(oldObj);
1079         obj.getParent().addChild(obj);
1080         return generalBeginRenameObject(oldObj, obj);
1081     }
1082 
1083     /**
1084      * Clean up cache state after a move operation and send any events that were missed.
1085      * @param oldParent The old parent object.
1086      * @param newParent The new parent object.
1087      * @param name The name of the object being moved.
1088      * @param success Whether operation was completed successfully.
1089      * @return Whether cache state is correct.
1090      */
endMoveObject(MtpObject oldParent, MtpObject newParent, String name, boolean success)1091     public synchronized boolean endMoveObject(MtpObject oldParent, MtpObject newParent, String name,
1092             boolean success) {
1093         if (sDebug)
1094             Log.v(TAG, "endMoveObject " + success);
1095         MtpObject oldObj = oldParent.getChild(name);
1096         MtpObject newObj = newParent.getChild(name);
1097         if (oldObj == null || newObj == null)
1098             return false;
1099         if (oldParent.getStorageId() != newObj.getStorageId()) {
1100             boolean ret = endRemoveObject(oldObj, success);
1101             return generalEndCopyObject(newObj, success, true) && ret;
1102         }
1103         if (!success) {
1104             // If the rename failed, we want oldObj to be the original and obj to be the stand-in.
1105             // Switch the objects, except for their parent and state.
1106             MtpObject temp = oldObj;
1107             MtpObjectState oldState = oldObj.getState();
1108             temp.setParent(newObj.getParent());
1109             temp.setState(newObj.getState());
1110             oldObj = newObj;
1111             oldObj.setParent(oldParent);
1112             oldObj.setState(oldState);
1113             newObj = temp;
1114             newObj.getParent().addChild(newObj);
1115             oldParent.addChild(oldObj);
1116         }
1117         return generalEndRenameObject(oldObj, newObj, success);
1118     }
1119 
1120     /**
1121      * Informs MtpStorageManager that the given object is about to be copied recursively.
1122      * @param object Object to be copied
1123      * @param newParent New parent for the object.
1124      * @return The object id for the new copy, or -1 if error.
1125      */
beginCopyObject(MtpObject object, MtpObject newParent)1126     public synchronized int beginCopyObject(MtpObject object, MtpObject newParent) {
1127         if (sDebug)
1128             Log.v(TAG, "beginCopyObject " + object.getName() + " to " + newParent.getPath());
1129         String name = object.getName();
1130         if (!newParent.isDir())
1131             return -1;
1132         if (newParent.isRoot() && mSubdirectories != null && !mSubdirectories.contains(name))
1133             return -1;
1134         getChildren(newParent); // Ensure parent is visited
1135         if (newParent.getChild(name) != null)
1136             return -1;
1137         MtpObject newObj  = object.copy(object.isDir());
1138         newParent.addChild(newObj);
1139         newObj.setParent(newParent);
1140         if (!generalBeginCopyObject(newObj, true))
1141             return -1;
1142         return newObj.getId();
1143     }
1144 
1145     /**
1146      * Cleans up cache state after a copy operation.
1147      * @param object Object that was copied.
1148      * @param success Whether the operation was successful.
1149      * @return Whether cache state is consistent.
1150      */
endCopyObject(MtpObject object, boolean success)1151     public synchronized boolean endCopyObject(MtpObject object, boolean success) {
1152         if (sDebug)
1153             Log.v(TAG, "endCopyObject " + object.getName() + " " + success);
1154         return generalEndCopyObject(object, success, false);
1155     }
1156 
generalEndAddObject(MtpObject obj, boolean succeeded, boolean removeGlobal)1157     private synchronized boolean generalEndAddObject(MtpObject obj, boolean succeeded,
1158             boolean removeGlobal) {
1159         switch (obj.getState()) {
1160             case FROZEN:
1161                 // Object was never created.
1162                 if (succeeded) {
1163                     // The operation was successful so the event must still be in the queue.
1164                     obj.setState(MtpObjectState.FROZEN_ONESHOT_ADD);
1165                 } else {
1166                     // The operation failed and never created the file.
1167                     if (!removeObjectFromCache(obj, removeGlobal, false)) {
1168                         return false;
1169                     }
1170                 }
1171                 break;
1172             case FROZEN_ADDED:
1173                 obj.setState(MtpObjectState.NORMAL);
1174                 if (!succeeded) {
1175                     MtpObject parent = obj.getParent();
1176                     // The operation failed but some other process created the file. Send an event.
1177                     if (!removeObjectFromCache(obj, removeGlobal, false))
1178                         return false;
1179                     handleAddedObject(parent, obj.getName(), obj.isDir());
1180                 }
1181                 // else: The operation successfully created the object.
1182                 break;
1183             case FROZEN_REMOVED:
1184                 if (!removeObjectFromCache(obj, removeGlobal, false))
1185                     return false;
1186                 if (succeeded) {
1187                     // Some other process deleted the object. Send an event.
1188                     mMtpNotifier.sendObjectRemoved(obj.getId());
1189                 }
1190                 // else: Mtp deleted the object as part of cleanup. Don't send an event.
1191                 break;
1192             default:
1193                 return false;
1194         }
1195         return true;
1196     }
1197 
generalEndRemoveObject(MtpObject obj, boolean success, boolean removeGlobal)1198     private synchronized boolean generalEndRemoveObject(MtpObject obj, boolean success,
1199             boolean removeGlobal) {
1200         switch (obj.getState()) {
1201             case FROZEN:
1202                 if (success) {
1203                     // Object was deleted successfully, and event is still in the queue.
1204                     obj.setState(MtpObjectState.FROZEN_ONESHOT_DEL);
1205                 } else {
1206                     // Object was not deleted.
1207                     obj.setState(MtpObjectState.NORMAL);
1208                 }
1209                 break;
1210             case FROZEN_ADDED:
1211                 // Object was deleted, and then readded.
1212                 obj.setState(MtpObjectState.NORMAL);
1213                 if (success) {
1214                     // Some other process readded the object.
1215                     MtpObject parent = obj.getParent();
1216                     if (!removeObjectFromCache(obj, removeGlobal, false))
1217                         return false;
1218                     handleAddedObject(parent, obj.getName(), obj.isDir());
1219                 }
1220                 // else : Object still exists after failure.
1221                 break;
1222             case FROZEN_REMOVED:
1223                 if (!removeObjectFromCache(obj, removeGlobal, false))
1224                     return false;
1225                 if (!success) {
1226                     // Some other process deleted the object.
1227                     mMtpNotifier.sendObjectRemoved(obj.getId());
1228                 }
1229                 // else : This process deleted the object as part of the operation.
1230                 break;
1231             default:
1232                 return false;
1233         }
1234         return true;
1235     }
1236 
generalBeginRenameObject(MtpObject fromObj, MtpObject toObj)1237     private synchronized boolean generalBeginRenameObject(MtpObject fromObj, MtpObject toObj) {
1238         fromObj.setState(MtpObjectState.FROZEN);
1239         toObj.setState(MtpObjectState.FROZEN);
1240         fromObj.setOperation(MtpOperation.RENAME);
1241         toObj.setOperation(MtpOperation.RENAME);
1242         return true;
1243     }
1244 
generalEndRenameObject(MtpObject fromObj, MtpObject toObj, boolean success)1245     private synchronized boolean generalEndRenameObject(MtpObject fromObj, MtpObject toObj,
1246             boolean success) {
1247         boolean ret = generalEndRemoveObject(fromObj, success, !success);
1248         return generalEndAddObject(toObj, success, success) && ret;
1249     }
1250 
generalBeginRemoveObject(MtpObject obj, MtpOperation op)1251     private synchronized boolean generalBeginRemoveObject(MtpObject obj, MtpOperation op) {
1252         obj.setState(MtpObjectState.FROZEN);
1253         obj.setOperation(op);
1254         if (obj.isDir()) {
1255             for (MtpObject child : obj.getChildren())
1256                 generalBeginRemoveObject(child, op);
1257         }
1258         return true;
1259     }
1260 
generalBeginCopyObject(MtpObject obj, boolean newId)1261     private synchronized boolean generalBeginCopyObject(MtpObject obj, boolean newId) {
1262         obj.setState(MtpObjectState.FROZEN);
1263         obj.setOperation(MtpOperation.COPY);
1264         if (newId) {
1265             obj.setId(getNextObjectId());
1266             mObjects.put(obj.getId(), obj);
1267         }
1268         if (obj.isDir())
1269             for (MtpObject child : obj.getChildren())
1270                 if (!generalBeginCopyObject(child, newId))
1271                     return false;
1272         return true;
1273     }
1274 
generalEndCopyObject(MtpObject obj, boolean success, boolean addGlobal)1275     private synchronized boolean generalEndCopyObject(MtpObject obj, boolean success, boolean addGlobal) {
1276         if (success && addGlobal)
1277             mObjects.put(obj.getId(), obj);
1278         boolean ret = true;
1279         if (obj.isDir()) {
1280             for (MtpObject child : new ArrayList<>(obj.getChildren())) {
1281                 if (child.getOperation() == MtpOperation.COPY)
1282                     ret = generalEndCopyObject(child, success, addGlobal) && ret;
1283             }
1284         }
1285         ret = generalEndAddObject(obj, success, success || !addGlobal) && ret;
1286         return ret;
1287     }
1288 }
1289