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