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.storage.StorageVolume; 22 import android.util.Log; 23 24 import java.io.IOException; 25 import java.nio.file.DirectoryIteratorException; 26 import java.nio.file.DirectoryStream; 27 import java.nio.file.Files; 28 import java.nio.file.Path; 29 import java.nio.file.Paths; 30 import java.util.ArrayList; 31 import java.util.Collection; 32 import java.util.HashMap; 33 import java.util.HashSet; 34 import java.util.Iterator; 35 import java.util.Objects; 36 import java.util.Set; 37 import 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 */ 45 public 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 MtpObjectObserver(MtpObject object)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 onEvent(int event, String path)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 finalize()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 MtpObject(String name, int id, MtpObject parent, boolean isDir)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 getName()162 public String getName() { 163 return mName; 164 } 165 getId()166 public int getId() { 167 return mId; 168 } 169 isDir()170 public boolean isDir() { 171 return mIsDir; 172 } 173 getFormat()174 public int getFormat() { 175 return mIsDir ? MtpConstants.FORMAT_ASSOCIATION : MediaFile.getFormatCode(mName, null); 176 } 177 getStorageId()178 public int getStorageId() { 179 return getRoot().getId(); 180 } 181 getModifiedTime()182 public long getModifiedTime() { 183 return getPath().toFile().lastModified() / 1000; 184 } 185 getParent()186 public MtpObject getParent() { 187 return mParent; 188 } 189 getRoot()190 public MtpObject getRoot() { 191 return isRoot() ? this : mParent.getRoot(); 192 } 193 getSize()194 public long getSize() { 195 return mIsDir ? 0 : getPath().toFile().length(); 196 } 197 getPath()198 public Path getPath() { 199 return isRoot() ? Paths.get(mName) : mParent.getPath().resolve(mName); 200 } 201 isRoot()202 public boolean isRoot() { 203 return mParent == null; 204 } 205 206 /** For MtpStorageManager only **/ 207 setName(String name)208 private void setName(String name) { 209 mName = name; 210 } 211 setId(int id)212 private void setId(int id) { 213 mId = id; 214 } 215 isVisited()216 private boolean isVisited() { 217 return mVisited; 218 } 219 setParent(MtpObject parent)220 private void setParent(MtpObject parent) { 221 mParent = parent; 222 } 223 setDir(boolean dir)224 private void setDir(boolean dir) { 225 if (dir != mIsDir) { 226 mIsDir = dir; 227 mChildren = mIsDir ? new HashMap<>() : null; 228 } 229 } 230 setVisited(boolean visited)231 private void setVisited(boolean visited) { 232 mVisited = visited; 233 } 234 getState()235 private MtpObjectState getState() { 236 return mState; 237 } 238 setState(MtpObjectState state)239 private void setState(MtpObjectState state) { 240 mState = state; 241 if (mState == MtpObjectState.NORMAL) 242 mOp = MtpOperation.NONE; 243 } 244 getOperation()245 private MtpOperation getOperation() { 246 return mOp; 247 } 248 setOperation(MtpOperation op)249 private void setOperation(MtpOperation op) { 250 mOp = op; 251 } 252 getObserver()253 private FileObserver getObserver() { 254 return mObserver; 255 } 256 setObserver(FileObserver observer)257 private void setObserver(FileObserver observer) { 258 mObserver = observer; 259 } 260 addChild(MtpObject child)261 private void addChild(MtpObject child) { 262 mChildren.put(child.getName(), child); 263 } 264 getChild(String name)265 private MtpObject getChild(String name) { 266 return mChildren.get(name); 267 } 268 getChildren()269 private Collection<MtpObject> getChildren() { 270 return mChildren.values(); 271 } 272 exists()273 private boolean exists() { 274 return getPath().toFile().exists(); 275 } 276 copy(boolean recursive)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 */ sendObjectAdded(int id)301 public abstract void sendObjectAdded(int id); 302 303 /** 304 * Called when an object is deleted. 305 */ sendObjectRemoved(int id)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 MtpStorageManager(MtpNotifier notifier, Set<String> subdirectories)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 */ close()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 */ setSubdirectories(Set<String> subDirs)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 */ addMtpStorage(StorageVolume volume)400 public synchronized MtpStorage addMtpStorage(StorageVolume volume) { 401 int storageId = ((getNextStorageId() & 0x0000FFFF) << 16) + 1; 402 MtpStorage storage = new MtpStorage(volume, storageId); 403 MtpObject root = new MtpObject(storage.getPath(), storageId, null, true); 404 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 */ removeMtpStorage(MtpStorage storage)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 */ isSpecialSubDir(MtpObject obj)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 */ getByPath(String path)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 */ getObject(int id)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 */ getStorageRoot(int id)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 getNextObjectId()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 getNextStorageId()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 */ getObjects(int parent, int format, int storageId)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 getObjects(MtpObject parent, int format, boolean rec)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 */ getChildren(MtpObject object)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 */ addObjectToCache(MtpObject parent, String newName, boolean isDir)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 */ removeObjectFromCache(MtpObject removed, boolean removeGlobal, boolean recursive)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 handleAddedObject(MtpObject parent, String path, boolean isDir)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 handleRemovedObject(MtpObject obj)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 */ flushEvents()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 */ dump()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 */ checkConsistency()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 */ beginSendObject(MtpObject parent, String name, int format)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 */ endSendObject(MtpObject obj, boolean succeeded)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 */ beginRenameObject(MtpObject obj, String newName)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 */ endRenameObject(MtpObject obj, String oldName, boolean success)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 */ beginRemoveObject(MtpObject obj)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 */ endRemoveObject(MtpObject obj, boolean success)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 */ beginMoveObject(MtpObject obj, MtpObject newParent)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 */ endMoveObject(MtpObject oldParent, MtpObject newParent, String name, boolean success)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 */ beginCopyObject(MtpObject object, MtpObject newParent)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 */ endCopyObject(MtpObject object, boolean success)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 generalEndAddObject(MtpObject obj, boolean succeeded, boolean removeGlobal)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 generalEndRemoveObject(MtpObject obj, boolean success, boolean removeGlobal)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 generalBeginRenameObject(MtpObject fromObj, MtpObject toObj)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 generalEndRenameObject(MtpObject fromObj, MtpObject toObj, boolean success)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 generalBeginRemoveObject(MtpObject obj, MtpOperation op)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 generalBeginCopyObject(MtpObject obj, boolean newId)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 generalEndCopyObject(MtpObject obj, boolean success, boolean addGlobal)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 } 1211