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