// made public for Upsource
  public RootIndex(@NotNull Project project, @NotNull InfoCache cache) {
    myProject = project;
    myInfoCache = cache;
    final RootInfo info = buildRootInfo(project);

    MultiMap<String, VirtualFile> rootsByPackagePrefix = MultiMap.create();
    Set<VirtualFile> allRoots = info.getAllRoots();
    for (VirtualFile root : allRoots) {
      List<VirtualFile> hierarchy = getHierarchy(root, allRoots, info);
      Pair<DirectoryInfo, String> pair =
          hierarchy != null
              ? calcDirectoryInfo(root, hierarchy, info)
              : new Pair<DirectoryInfo, String>(NonProjectDirectoryInfo.IGNORED, null);
      cacheInfos(root, root, pair.first);
      rootsByPackagePrefix.putValue(pair.second, root);
      myPackagePrefixByRoot.put(root, pair.second);
    }
    myPackageDirectoryCache =
        new PackageDirectoryCache(rootsByPackagePrefix) {
          @Override
          protected boolean isPackageDirectory(
              @NotNull VirtualFile dir, @NotNull String packageName) {
            return getInfoForFile(dir).isInProject() && packageName.equals(getPackageName(dir));
          }
        };
  }
  private static OrderEntry[] calcOrderEntries(
      @NotNull RootInfo info,
      @NotNull MultiMap<VirtualFile, OrderEntry> depEntries,
      @NotNull MultiMap<VirtualFile, OrderEntry> libClassRootEntries,
      @NotNull MultiMap<VirtualFile, OrderEntry> libSourceRootEntries,
      @NotNull List<VirtualFile> hierarchy) {
    @Nullable VirtualFile libraryClassRoot = info.findLibraryRootInfo(hierarchy, false);
    @Nullable VirtualFile librarySourceRoot = info.findLibraryRootInfo(hierarchy, true);
    Set<OrderEntry> orderEntries = ContainerUtil.newLinkedHashSet();
    orderEntries.addAll(
        info.getLibraryOrderEntries(
            hierarchy,
            libraryClassRoot,
            librarySourceRoot,
            libClassRootEntries,
            libSourceRootEntries));
    for (VirtualFile root : hierarchy) {
      orderEntries.addAll(depEntries.get(root));
    }
    VirtualFile moduleContentRoot = info.findModuleRootInfo(hierarchy);
    if (moduleContentRoot != null) {
      ContainerUtil.addIfNotNull(
          orderEntries,
          info.getModuleSourceEntry(hierarchy, moduleContentRoot, libClassRootEntries));
    }
    if (orderEntries.isEmpty()) {
      return null;
    }

    OrderEntry[] array = orderEntries.toArray(new OrderEntry[orderEntries.size()]);
    Arrays.sort(array, BY_OWNER_MODULE);
    return array;
  }
  @NotNull
  private Map<VirtualFile, OrderEntry[]> getOrderEntries() {
    Map<VirtualFile, OrderEntry[]> result = myOrderEntries;
    if (result != null) return result;

    MultiMap<VirtualFile, OrderEntry> libClassRootEntries = MultiMap.createSmart();
    MultiMap<VirtualFile, OrderEntry> libSourceRootEntries = MultiMap.createSmart();
    MultiMap<VirtualFile, OrderEntry> depEntries = MultiMap.createSmart();

    for (final Module module : ModuleManager.getInstance(myProject).getModules()) {
      final ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
      for (OrderEntry orderEntry : moduleRootManager.getOrderEntries()) {
        if (orderEntry instanceof ModuleOrderEntry) {
          final Module depModule = ((ModuleOrderEntry) orderEntry).getModule();
          if (depModule != null) {
            VirtualFile[] importedClassRoots =
                OrderEnumerator.orderEntries(depModule)
                    .exportedOnly()
                    .recursively()
                    .classes()
                    .usingCache()
                    .getRoots();
            for (VirtualFile importedClassRoot : importedClassRoots) {
              depEntries.putValue(importedClassRoot, orderEntry);
            }
          }
          for (VirtualFile sourceRoot : orderEntry.getFiles(OrderRootType.SOURCES)) {
            depEntries.putValue(sourceRoot, orderEntry);
          }
        } else if (orderEntry instanceof LibraryOrSdkOrderEntry) {
          final LibraryOrSdkOrderEntry entry = (LibraryOrSdkOrderEntry) orderEntry;
          for (final VirtualFile sourceRoot : entry.getRootFiles(OrderRootType.SOURCES)) {
            libSourceRootEntries.putValue(sourceRoot, orderEntry);
          }
          for (final VirtualFile classRoot : entry.getRootFiles(OrderRootType.CLASSES)) {
            libClassRootEntries.putValue(classRoot, orderEntry);
          }
        }
      }
    }

    RootInfo rootInfo = buildRootInfo(myProject);
    result = ContainerUtil.newHashMap();
    Set<VirtualFile> allRoots = rootInfo.getAllRoots();
    for (VirtualFile file : allRoots) {
      List<VirtualFile> hierarchy = getHierarchy(file, allRoots, rootInfo);
      result.put(
          file,
          hierarchy == null
              ? OrderEntry.EMPTY_ARRAY
              : calcOrderEntries(
                  rootInfo, depEntries, libClassRootEntries, libSourceRootEntries, hierarchy));
    }
    myOrderEntries = result;
    return result;
  }
  @NotNull
  private static Pair<DirectoryInfo, String> calcDirectoryInfo(
      @NotNull final VirtualFile root,
      @NotNull final List<VirtualFile> hierarchy,
      @NotNull RootInfo info) {
    VirtualFile moduleContentRoot = info.findModuleRootInfo(hierarchy);
    VirtualFile libraryClassRoot = info.findLibraryRootInfo(hierarchy, false);
    VirtualFile librarySourceRoot = info.findLibraryRootInfo(hierarchy, true);
    boolean inProject =
        moduleContentRoot != null || libraryClassRoot != null || librarySourceRoot != null;
    VirtualFile nearestContentRoot;
    if (inProject) {
      nearestContentRoot = moduleContentRoot;
    } else {
      nearestContentRoot = info.findNearestContentRootForExcluded(hierarchy);
      if (nearestContentRoot == null) {
        return new Pair<DirectoryInfo, String>(NonProjectDirectoryInfo.EXCLUDED, null);
      }
    }

    VirtualFile sourceRoot =
        info.findPackageRootInfo(hierarchy, moduleContentRoot, null, librarySourceRoot);

    VirtualFile moduleSourceRoot =
        info.findPackageRootInfo(hierarchy, moduleContentRoot, null, null);
    boolean inModuleSources = moduleSourceRoot != null;
    boolean inLibrarySource = librarySourceRoot != null;
    int typeId = moduleSourceRoot != null ? info.rootTypeId.get(moduleSourceRoot) : 0;

    Module module = info.contentRootOf.get(nearestContentRoot);
    DirectoryInfo directoryInfo =
        new DirectoryInfoImpl(
            root,
            module,
            nearestContentRoot,
            sourceRoot,
            libraryClassRoot,
            inModuleSources,
            inLibrarySource,
            !inProject,
            typeId);

    String packagePrefix =
        info.calcPackagePrefix(
            root, hierarchy, moduleContentRoot, libraryClassRoot, librarySourceRoot);

    return Pair.create(directoryInfo, packagePrefix);
  }
  private void updateVolumesLocked() {
    mRoots.clear();

    final int userId = UserHandle.myUserId();
    final List<VolumeInfo> volumes = mStorageManager.getVolumes();
    for (VolumeInfo volume : volumes) {
      if (!volume.isMountedReadable()) continue;

      final String rootId;
      final String title;
      if (volume.getType() == VolumeInfo.TYPE_EMULATED) {
        // We currently only support a single emulated volume mounted at
        // a time, and it's always considered the primary
        rootId = ROOT_ID_PRIMARY_EMULATED;
        if (VolumeInfo.ID_EMULATED_INTERNAL.equals(volume.getId())) {
          title = getContext().getString(R.string.root_internal_storage);
        } else {
          final VolumeInfo privateVol = mStorageManager.findPrivateForEmulated(volume);
          title = mStorageManager.getBestVolumeDescription(privateVol);
        }
      } else if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
        rootId = volume.getFsUuid();
        title = mStorageManager.getBestVolumeDescription(volume);
      } else {
        // Unsupported volume; ignore
        continue;
      }

      if (TextUtils.isEmpty(rootId)) {
        Log.d(TAG, "Missing UUID for " + volume.getId() + "; skipping");
        continue;
      }
      if (mRoots.containsKey(rootId)) {
        Log.w(TAG, "Duplicate UUID " + rootId + " for " + volume.getId() + "; skipping");
        continue;
      }

      try {
        final RootInfo root = new RootInfo();
        mRoots.put(rootId, root);

        root.rootId = rootId;
        root.flags =
            Root.FLAG_SUPPORTS_CREATE
                | Root.FLAG_LOCAL_ONLY
                | Root.FLAG_ADVANCED
                | Root.FLAG_SUPPORTS_SEARCH
                | Root.FLAG_SUPPORTS_IS_CHILD;
        root.title = title;
        if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
          root.flags |= Root.FLAG_HAS_SETTINGS;
        }
        if (volume.isVisibleForRead(userId)) {
          root.visiblePath = volume.getPathForUser(userId);
        } else {
          root.visiblePath = null;
        }
        root.path = volume.getInternalPathForUser(userId);
        root.docId = getDocIdForFile(root.path);

      } catch (FileNotFoundException e) {
        throw new IllegalStateException(e);
      }
    }

    Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots");

    // Note this affects content://com.android.externalstorage.documents/root/39BD-07C5
    // as well as content://com.android.externalstorage.documents/document/*/children,
    // so just notify on content://com.android.externalstorage.documents/.
    getContext().getContentResolver().notifyChange(BASE_URI, null, false);
  }