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