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