/** * DiskUsage - displays sdcard usage on android. * Copyright (C) 2008-2011 Ivan Volosyuk * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package com.google.android.diskusage; import java.io.BufferedReader; import java.io.File; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.TreeMap; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.util.Log; import com.google.android.diskusage.datasource.DataSource; import com.google.android.diskusage.datasource.PortableFile; import com.google.android.diskusage.datasource.StatFsSource; import com.google.android.diskusage.entity.FileSystemEntry; import com.google.android.diskusage.entity.FileSystemEntry.ExcludeFilter; import com.google.android.diskusage.entity.FileSystemRoot; public class MountPoint { final FileSystemEntry.ExcludeFilter excludeFilter; String title; final String root; final boolean hasApps2SD; final boolean rootRequired; final boolean forceHasApps; final String fsType; MountPoint(String title, String root, ExcludeFilter excludeFilter, boolean hasApps2SD, boolean rootRequired, String fsType, boolean forceHasApps) { this.title = title; this.root = root; this.excludeFilter = excludeFilter; this.hasApps2SD = hasApps2SD; this.rootRequired = rootRequired; this.fsType = fsType; this.forceHasApps = forceHasApps; } private static MountPoint defaultStorage; private static Map<String, MountPoint> mountPoints = new TreeMap<String, MountPoint>(); private static Map<String, MountPoint> rootedMountPoints = new TreeMap<String, MountPoint>(); private static List<MountPoint> storageMountPoints = new ArrayList<MountPoint>(); private static boolean init = false; private static MountPoint honeycombSdcard; static int checksum = 0; public static MountPoint getHoneycombSdcard(Context context) { initMountPoints(context); return honeycombSdcard; } public static MountPoint getDefaultStorage(Context context) { initMountPoints(context); return defaultStorage; } public static Map<String,MountPoint> getMountPoints(Context context) { initMountPoints(context); return mountPoints; } public static Map<String,MountPoint> getRootedMountPoints(Context context) { initMountPoints(context); return rootedMountPoints; } public static MountPoint getNormal(Context context, String rootPath) { initMountPoints(context); return mountPoints.get(rootPath); } public static MountPoint forPath(Context context, String path) { Log.d("diskusage", "Looking for mount point for path: " + path); initMountPoints(context); MountPoint match = null; path = FileSystemRoot.withSlash(path); for (MountPoint m : mountPoints.values()) { if (path.contains(FileSystemRoot.withSlash(m.root))) { if (match == null || match.root.length() < m.root.length()) { Log.d("diskusage", "MATCH:" + m.root); match = m; } } } for (MountPoint m : rootedMountPoints.values()) { if (path.contains(FileSystemRoot.withSlash(m.root))) { if (match == null || match.root.length() < m.root.length()) { match = m; Log.d("diskusage", "MATCH:" + m.root); } } } // FIXME: quick hack if (match == null) { Log.d("diskusage", "Use honeycomb hack for /data"); match = mountPoints.get("/data"); } return match; } public static MountPoint getRooted(Context context, String rootPath) { initMountPoints(context); return rootedMountPoints.get(rootPath); } public static boolean hasMultiple(Context context) { initMountPoints(context); return mountPoints.size() != 1; } public FileSystemEntry.ExcludeFilter getExcludeFilter() { return excludeFilter; } public String getRoot() { return root; } public static String storageCardPath() { PortableFile externalStorageDirectory = DataSource.get().getExternalStorageDirectory(); return externalStorageDirectory.getCanonicalPath(); } private static boolean isEmulated(String fsType) { return fsType.equals("sdcardfs") || fsType.equals("fuse"); } private static void initMountPoints(Context context) { if (init) return; init = true; String storagePath = storageCardPath(); Log.d("diskusage", "StoragePath: " + storagePath); ArrayList<MountPoint> mountPointsList = new ArrayList<MountPoint>(); HashSet<String> excludePoints = new HashSet<String>(); if (storagePath != null) { defaultStorage = new MountPoint( titleStorageCard(context), storagePath, null, false, false, "", false); mountPointsList.add(defaultStorage); mountPoints.put(storagePath, defaultStorage); } try { // FIXME: debug checksum = 0; BufferedReader reader = DataSource.get().getProcReader(); String line; while ((line = reader.readLine()) != null) { checksum += line.length(); Log.d("diskusage", "line: " + line); String[] parts = line.split(" +"); if (parts.length < 3) continue; String mountPoint = parts[1]; Log.d("diskusage", "Mount point: " + mountPoint); String fsType = parts[2]; StatFsSource stat = null; try { stat = DataSource.get().statFs(mountPoint); } catch (Exception e) { } if (!(fsType.equals("vfat") || fsType.equals("tntfs") || fsType.equals("exfat") || fsType.equals("texfat") || isEmulated(fsType)) || mountPoint.startsWith("/mnt/asec") || mountPoint.startsWith("/firmware") || mountPoint.startsWith("/mnt/secure") || mountPoint.startsWith("/data/mac") || stat == null || (mountPoint.endsWith("/legacy") && isEmulated(fsType))) { Log.d("diskusage", String.format("Excluded based on fsType=%s or black list", fsType)); excludePoints.add(mountPoint); // Default storage is not vfat, removing it (real honeycomb) if (mountPoint.equals(storagePath)) { mountPointsList.remove(defaultStorage); mountPoints.remove(mountPoint); } if (/*rooted &&*/ !mountPoint.startsWith("/mnt/asec/")) { mountPointsList.add(new MountPoint(mountPoint, mountPoint, null, false, true, fsType, false)); } } else { Log.d("diskusage", "Mount point is good"); MountPoint mountPointObj = new MountPoint(mountPoint, mountPoint, null, false, false, fsType, false); mountPointsList.add(mountPointObj); if (mountPoint.startsWith("/storage/") && !mountPoint.startsWith("/storage/emulated")) { storageMountPoints.add(mountPointObj); } } } reader.close(); for (MountPoint mountPoint: mountPointsList) { String prefix = mountPoint.root + "/"; boolean has_apps2sd = false; ArrayList<String> excludes = new ArrayList<String>(); String mountPointName = new File(mountPoint.root).getName(); for (MountPoint otherMountPoint : mountPointsList) { if (otherMountPoint.root.startsWith(prefix)) { excludes.add(mountPointName + "/" + otherMountPoint.root.substring(prefix.length())); } } for (String otherMountPoint : excludePoints) { if (otherMountPoint.equals(prefix + ".android_secure")) { has_apps2sd = true; } if (otherMountPoint.startsWith(prefix)) { excludes.add(mountPointName + "/" + otherMountPoint.substring(prefix.length())); } } MountPoint newMountPoint = new MountPoint( mountPoint.root, mountPoint.root, new ExcludeFilter(excludes), has_apps2sd, mountPoint.rootRequired, mountPoint.fsType, false); if (mountPoint.rootRequired) { rootedMountPoints.put(mountPoint.root, newMountPoint); } else { mountPoints.put(mountPoint.root, newMountPoint); } } } catch (Exception e) { Log.e("diskusage", "Failed to get mount points", e); } final int sdkVersion = DataSource.get().getAndroidVersion(); try { addMediaPaths(context); } catch (Throwable t) { Log.e("diskusage", "Adding media paths", t); } MountPoint storageCard = mountPoints.get(storageCardPath()); if(sdkVersion >= Build.VERSION_CODES.HONEYCOMB && (storageCard == null || isEmulated(storageCard.fsType))) { mountPoints.remove(storageCardPath()); // No real /sdcard in honeycomb honeycombSdcard = defaultStorage; mountPoints.put("/data", new MountPoint( titleStorageCard(context), "/data", null, false, false, "", true)); } if (!mountPoints.isEmpty()) { defaultStorage = mountPoints.values().iterator().next(); defaultStorage.title = titleStorageCard(context); } if (sdkVersion >= Build.VERSION_CODES.LOLLIPOP) { initMountPointsLollipop(context); } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static PortableFile getBaseDir(PortableFile dir) { if (dir == null) { return null; } long totalSpace = dir.getTotalSpace(); while (true) { PortableFile base = DataSource.get().getParentFile(dir); try { base.isExternalStorageEmulated(); } catch (Exception e) { return dir; } if (base == null || dir.equals(base) || base.getTotalSpace() != totalSpace) { return dir; } dir = base; } } // Lollipop have new API to get storage states, which can try to use it instead complicated // legacy stuff. @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static void initMountPointsLollipop(Context context) { Map<String, MountPoint> mountPoints = new TreeMap<String, MountPoint>(); PortableFile defaultDir = getBaseDir(DataSource.get().getExternalFilesDir(context)); PortableFile[] dirs = DataSource.get().getExternalFilesDirs(context); for (PortableFile path : dirs) { if (path == null) { continue; } PortableFile dir = getBaseDir(path); boolean isEmulated = path.isExternalStorageEmulated(); boolean isRemovable = path.isExternalStorageRemovable(); boolean hasApps = isEmulated && !isRemovable; MountPoint mountPoint = new MountPoint( dir.equals(defaultDir) ? titleStorageCard(context) : dir.getAbsolutePath(), dir.getAbsolutePath(), new ExcludeFilter(new ArrayList<String>()), false /* hasApps2SD */, false /* rootRequired */, "whoCares", hasApps /* forceHasApps */); mountPoints.put(mountPoint.root, mountPoint); if (!isRemovable) { defaultStorage = mountPoint; honeycombSdcard = mountPoint; } } for (MountPoint m : storageMountPoints) { if (!mountPoints.containsKey(m.root)) { mountPoints.put(m.root, m); } } MountPoint.mountPoints = mountPoints; } @TargetApi(Build.VERSION_CODES.KITKAT) public static PortableFile[] getMediaStoragePaths(Context context) { try { return DataSource.get().getExternalFilesDirs(context); } catch (Throwable t) { return new PortableFile[0]; } } private static void addMediaPaths(Context context) { PortableFile[] mediaStoragePaths = getMediaStoragePaths(context); for (PortableFile file : mediaStoragePaths) { while (file != null) { String canonical = file.getCanonicalPath(); if (mountPoints.containsKey(canonical)) { break; } MountPoint rootedMountPoint = rootedMountPoints.get(canonical); if (rootedMountPoint != null) { mountPoints.put(canonical, new MountPoint( canonical, canonical, null, false, false, rootedMountPoint.fsType, false)); break; } if (canonical.equals("/")) break; file = DataSource.get().getParentFile(file); } } } private static String titleStorageCard(Context context) { return context.getString(R.string.storage_card); } // private static final String file = // "rootfs / rootfs ro,relatime 0 0\n" + // "tmpfs /dev tmpfs rw,relatime,mode=755 0 0\n" + // "devpts /dev/pts devpts rw,relatime,mode=600 0 0\n" + // "proc /proc proc rw,relatime 0 0\n" + // "sysfs /sys sysfs rw,relatime 0 0\n" + // "none /acct cgroup rw,relatime,cpuacct 0 0\n" + // "tmpfs /mnt/asec tmpfs rw,relatime,mode=755,gid=1000 0 0\n" + // "none /dev/cpuctl cgroup rw,relatime,cpu 0 0\n" + // "/dev/block/mtdblock3 /system yaffs2 ro,relatime 0 0\n" + // "/dev/block/mtdblock5 /data yaffs2 rw,nosuid,nodev,relatime 0 0\n" + // "/dev/block/mtdblock4 /cache yaffs2 rw,nosuid,nodev,relatime 0 0\n" + // "/sys/kernel/debug /sys/kernel/debug debugfs rw,relatime 0 0\n" + // "/dev/block/vold/179:1 /mnt/sdcard vfat rw,dirsync,nosuid,nodev,noexec,relatime,uid=1000,gid=1015,fmask=0702,dmask=0702,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0\n" + // "/dev/block/vold/179:1 /mnt/secure/asec vfat rw,dirsync,nosuid,nodev,noexec,relatime,uid=1000,gid=1015,fmask=0702,dmask=0702,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0\n" + // "tmpfs /mnt/sdcard/.android_secure tmpfs ro,relatime,size=0k,mode=000 0 0\n" + // "/dev/block/dm-0 /mnt/asec/com.dasur.language.rus.pack-1 vfat ro,dirsync,nosuid,nodev,noexec,relatime,uid=1000,fmask=0222,dmask=0222,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0\n" + // "/dev/block/dm-1 /mnt/asec/net.bytten.xkcdviewer-1 vfat ro,dirsync,nosuid,nodev,noexec,relatime,uid=1000,fmask=0222,dmask=0222,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0\n" + // "/dev/block/dm-2 /mnt/asec/zok.android.shapes-2 vfat ro,dirsync,nosuid,nodev,noexec,relatime,uid=1000,fmask=0222,dmask=0222,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0\n" + // "/dev/block/dm-3 /mnt/asec/net.hexage.everlands-1 vfat ro,dirsync,nosuid,nodev,noexec,relatime,uid=1000,fmask=0222,dmask=0222,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0\n" + // "/dev/block/dm-3 /mnt/sdcard/maps ext2 ro,relatime,size=0k,mode=000 0 0\n"; public static void reset() { defaultStorage = null; honeycombSdcard = null; mountPoints = new TreeMap<String, MountPoint>(); rootedMountPoints = new TreeMap<String, MountPoint>(); storageMountPoints = new ArrayList<MountPoint>(); init = false; } }