/**
* DiskUsage - displays sdcard usage on android.
* Copyright (C) 2008 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.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.diskusage.datasource.DataSource;
import com.google.android.diskusage.datasource.StatFsSource;
import com.google.android.diskusage.entity.FileSystemEntry;
import com.google.android.diskusage.entity.FileSystemEntrySmall;
import com.google.android.diskusage.entity.FileSystemFreeSpace;
import com.google.android.diskusage.entity.FileSystemPackage;
import com.google.android.diskusage.entity.FileSystemRoot;
import com.google.android.diskusage.entity.FileSystemSuperRoot;
import com.google.android.diskusage.entity.FileSystemSystemSpace;
import com.google.android.diskusage.utils.MimeTypes;
public class DiskUsage extends LoadableActivity {
// FIXME: wrap to direct requests to rendering thread
protected FileSystemState fileSystemState;
public static final int RESULT_DELETE_CONFIRMED = 10;
public static final int RESULT_DELETE_CANCELED = 11;
public static final String STATE_KEY = "state";
public static final String TITLE_KEY = "title";
public static final String ROOT_KEY = "root";
public static final String KEY_KEY = "key";
public static final String DELETE_PATH_KEY = "path";
public static final String DELETE_ABSOLUTE_PATH_KEY = "absolute_path";
private String pathToDelete;
private String rootPath;
private String rootTitle;
String key;
private static final MimeTypes mimeTypes = new MimeTypes();
DiskUsageMenu menu = DiskUsageMenu.getInstance(this);
RendererManager rendererManager = new RendererManager(this);
private void initFromUri(String inpath) throws IOException {
final String path = new File(inpath).getCanonicalPath();
Log.d("diskusage", "Specified file: " + inpath + " absolute: " + path);
MountPoint mountPoint = MountPoint.forPath(this, path);
rootPath = mountPoint.root;
Log.d("diskusage", "rootPath = " + rootPath);
rootTitle = mountPoint.title;
key = SelectActivity.getKeyForStorage(mountPoint);
afterLoadAction.add(new Runnable() {
@Override
public void run() {
fileSystemState.selectFileInRendererThread(path);
}
});
}
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
Log.d("diskusage", "onCreate()");
setContentView(new TextView(this));
menu.onCreate();
Intent i = getIntent();
if ("com.google.android.diskusage.VIEW".equals(i.getAction())) {
try {
initFromUri(i.getData().getPath());
} catch (Exception e) {
Log.e("diskusage", "Failed to find specified file", e);
finish();
return;
}
} else {
rootPath = i.getStringExtra(ROOT_KEY);
rootTitle = i.getStringExtra(TITLE_KEY);
key = i.getStringExtra(KEY_KEY);
}
Bundle receivedState = i.getBundleExtra(STATE_KEY);
Log.d("diskusage", "onCreate, rootPath = " + rootPath + " receivedState = " + receivedState);
if (receivedState != null) onRestoreInstanceState(receivedState);
if (key == null) {
// Just close instead of crashing later
finish();
}
}
public boolean isMergedStorage() {
final int sdkVersion = DataSource.get().getAndroidVersion();
return sdkVersion >= Build.VERSION_CODES.HONEYCOMB;
}
ArrayList<Runnable> afterLoadAction = new ArrayList<Runnable>();
public void applyPatternNewRoot(FileSystemSuperRoot newRoot, String searchQuery) {
fileSystemState.replaceRootKeepCursor(newRoot, searchQuery);
}
@Override
protected void onResume() {
super.onResume();
rendererManager.onResume();
if (pkg_removed != null) {
// Check if package removed
String pkg_name = pkg_removed.pkg;
PackageManager pm = getPackageManager();
try {
pm.getPackageInfo(pkg_name, 0);
} catch (NameNotFoundException e) {
if (fileSystemState != null)
fileSystemState.removeInRenderThread(pkg_removed);
}
pkg_removed = null;
}
LoadFiles(this, new AfterLoad() {
@Override
public void run(final FileSystemSuperRoot root, boolean isCached) {
fileSystemState = new FileSystemState(DiskUsage.this, root);
rendererManager.makeView(fileSystemState, root);
fileSystemState.startZoomAnimationInRenderThread(null, !isCached, false);
for (Runnable r : afterLoadAction) {
r.run();
}
afterLoadAction.clear();
if (pathToDelete != null) {
String path = pathToDelete;
pathToDelete = null;
continueDelete(path);
}
}
}, false);
}
@Override
protected void onPause() {
rendererManager.onPause();
super.onPause();
if (fileSystemState != null) {
fileSystemState.killRenderThread();
final Bundle savedState = new Bundle();
fileSystemState.saveState(savedState);
afterLoadAction.add(new Runnable() {
@Override
public void run() {
fileSystemState.restoreStateInRenderThread(savedState);
}
});
}
}
@Override
public void onActivityResult(int a, int result, Intent i) {
if (result != RESULT_DELETE_CONFIRMED) return;
pathToDelete = i.getStringExtra("path");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return super.onCreateOptionsMenu(menu);
}
private static abstract class VersionedPackageViewer {
abstract void viewPackage(String pkg);
public static VersionedPackageViewer newInstance(DiskUsage context) {
final int sdkVersion = DataSource.get().getAndroidVersion();
VersionedPackageViewer viewer = null;
if (sdkVersion < Build.VERSION_CODES.GINGERBREAD) {
viewer = context.new EclairPackageViewer();
} else {
viewer = context.new GingerbreadPackageViewer();
}
return viewer;
}
}
private final class EclairPackageViewer extends VersionedPackageViewer {
@Override
void viewPackage(String pkg) {
try {
final String APP_PKG_PREFIX = "com.android.settings.";
final String APP_PKG_NAME = APP_PKG_PREFIX+"ApplicationPkgName";
Log.d("diskusage", "show package = " + pkg);
Intent viewIntent = new Intent(Intent.ACTION_VIEW);
viewIntent.setComponent(new ComponentName(
"com.android.settings", "com.android.settings.InstalledAppDetails"));
viewIntent.putExtra(APP_PKG_NAME, pkg);
viewIntent.putExtra("pkg", pkg);
startActivity(viewIntent);
} catch (RuntimeException e) {
Toast.makeText(DiskUsage.this,
"Sorry, failed to view the installed app. " +
"Please contact app developer.", Toast.LENGTH_SHORT).show();
}
}
}
private final class GingerbreadPackageViewer extends VersionedPackageViewer {
@Override
void viewPackage(String pkg) {
Log.d("diskusage", "show package = " + pkg);
Intent viewIntent = new Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:" + pkg));
startActivity(viewIntent);
}
}
private final VersionedPackageViewer packageViewer =
VersionedPackageViewer.newInstance(this);
protected void viewPackage(FileSystemPackage pkg) {
packageViewer.viewPackage(pkg.pkg);
// FIXME: reload package data instead of just removing it
pkg_removed = pkg;
}
void continueDelete(String path) {
FileSystemEntry entry = fileSystemState.masterRoot.getEntryByName(path, true);
if (entry != null) {
BackgroundDelete.startDelete(this, entry);
} else {
Toast.makeText(this,
"Oops. Can't find directory to be deleted.", Toast.LENGTH_SHORT);
}
}
public void askForDeletion(final FileSystemEntry entry) {
final String path = entry.path2();
final String fullPath = entry.absolutePath();
Log.d("DiskUsage", "Deletion requested for " + path);
if (entry instanceof FileSystemEntrySmall) {
Toast.makeText(this,
"Delete directory instead", Toast.LENGTH_SHORT).show();
return;
}
if (entry.children == null || entry.children.length == 0) {
if (entry instanceof FileSystemPackage) {
this.pkg_removed = (FileSystemPackage) entry;
BackgroundDelete.startDelete(this, entry);
return;
}
// Delete single file or directory
new AlertDialog.Builder(this)
.setTitle(new File(fullPath).isDirectory()
? format(R.string.ask_to_delete_directory, path)
: format(R.string.ask_to_delete_file, path))
.setPositiveButton(str(R.string.button_delete),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
BackgroundDelete.startDelete(DiskUsage.this, entry);
}
})
.setNegativeButton(str(R.string.button_cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int whichButton) {
}
}).create().show();
} else {
Intent i = new Intent(this, DeleteActivity.class);
i.putExtra(DELETE_PATH_KEY, path);
i.putExtra(DELETE_ABSOLUTE_PATH_KEY, fullPath);
i.putExtra(DeleteActivity.NUM_FILES_KEY, entry.getNumFiles());
i.putExtra(DiskUsage.KEY_KEY, this.key);
i.putExtra(DiskUsage.TITLE_KEY, this.getRootTitle());
i.putExtra(DiskUsage.ROOT_KEY, this.getRootPath());
i.putExtra(DeleteActivity.SIZE_KEY, entry.sizeString());
this.startActivityForResult(i, 0);
}
}
private String format(int id, Object... args) {
return getString(id, args);
}
private String str(int id) {
return getString(id);
}
public boolean isIntentAvailable(Intent intent) {
final PackageManager packageManager = getPackageManager();
List<ResolveInfo> res = packageManager.queryIntentActivities(
intent, PackageManager.MATCH_DEFAULT_ONLY);
return res.size() > 0;
}
public void view(FileSystemEntry entry) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (entry instanceof FileSystemEntrySmall) {
entry = entry.parent;
}
if (entry instanceof FileSystemPackage) {
viewPackage((FileSystemPackage) entry);
return;
}
if (entry.parent != null && entry.parent instanceof FileSystemPackage) {
viewPackage((FileSystemPackage) entry.parent);
return;
}
File file = new File(entry.absolutePath());
Uri uri = Uri.fromFile(file);
if (file.isDirectory()) {
// intent = new Intent(Intent.ACTION_GET_CONTENT);
// intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// intent.setDataAndType(uri, "file/*");
//
// try {
// startActivity(intent);
// return;
// } catch(ActivityNotFoundException e) {
// }
intent = new Intent("org.openintents.action.VIEW_DIRECTORY");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(uri);
try {
startActivity(intent);
return;
} catch(ActivityNotFoundException e) {
}
intent = new Intent("org.openintents.action.PICK_DIRECTORY");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(uri);
intent.putExtra("org.openintents.extra.TITLE",
str(R.string.title_in_oi_file_manager));
intent.putExtra("org.openintents.extra.BUTTON_TEXT",
str(R.string.button_text_in_oi_file_manager));
try {
startActivity(intent);
return;
} catch(ActivityNotFoundException e) {
}
// old Astro
intent = new Intent(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(uri, "vnd.android.cursor.item/com.metago.filemanager.dir");
try {
startActivity(intent);
return;
} catch(ActivityNotFoundException e) {
}
final Intent installSolidExplorer = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=pl.solidexplorer"));
if (isIntentAvailable(installSolidExplorer)) {
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle("Missing compatible file manager")
.setMessage("No compatible filemanager found.\n\nAsk you favorite file manager developer " +
"for integration with DiskUsage or install:" +
"\n * Solid Explorer" +
"\n * OI File Manager")
.setPositiveButton("Install Solid Explorer", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
startActivity(installSolidExplorer);
}
})
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
// pass
}
})
.create().show();
} else {
Toast.makeText(this, str(R.string.install_oi_file_manager),
Toast.LENGTH_SHORT).show();
}
return;
}
String fileName = entry.name;
int dot = fileName.lastIndexOf(".");
if (dot != -1) {
String extension = fileName.substring(dot + 1).toLowerCase();
String mime = mimeTypes.getMimeByExtension(this, extension);
try {
if (mime != null) {
intent.setDataAndType(uri, mime);
} else {
intent.setDataAndType(uri, "binary/octet-stream");
}
startActivity(intent);
return;
} catch (ActivityNotFoundException e) {
}
}
Toast.makeText(this, str(R.string.no_viewer_found),
Toast.LENGTH_SHORT).show();
}
public void rescan() {
LoadFiles(DiskUsage.this, new AfterLoad() {
@Override
public void run(FileSystemSuperRoot newRoot, boolean isCached) {
fileSystemState.startZoomAnimationInRenderThread(newRoot, !isCached, false);
}
}, true);
}
public void showFilterDialog() {
Intent i = new Intent(this, FilterActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResult(i, 0);
}
public void finishOnBack() {
if (!menu.readyToFinish()) {
return;
}
Bundle outState = new Bundle();
onSaveInstanceState(outState);
Intent result = new Intent();
result.putExtra(DiskUsage.STATE_KEY, outState);
result.putExtra(DiskUsage.KEY_KEY, key);
setResult(0, result);
finish();
}
public void setSelectedEntity(FileSystemEntry position) {
menu.update(position);
setTitle(format(R.string.title_for_path, position.toTitleString()));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finishOnBack();
break;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
if (fileSystemState == null) return;
fileSystemState.killRenderThread();
fileSystemState.saveState(outState);
menu.onSaveInstanceState(outState);
}
@Override
protected void onRestoreInstanceState(final Bundle inState) {
Log.d("diskusage", "onRestoreInstanceState, rootPath = " + inState.getString(ROOT_KEY));
if (fileSystemState != null)
fileSystemState.restoreStateInRenderThread(inState);
else {
afterLoadAction.add(new Runnable() {
@Override
public void run() {
fileSystemState.restoreStateInRenderThread(inState);
}
});
}
menu.onRestoreInstanceState(inState);
}
public interface AfterLoad {
public void run(FileSystemSuperRoot root, boolean isCached);
}
Handler handler = new Handler();
private Runnable progressUpdater;
static abstract class MemoryClass {
abstract int maxHeap();
static class MemoryClassDefault extends MemoryClass {
@Override
int maxHeap() {
return 16 * 1024 * 1024;
}
};
static MemoryClass getInstance(DiskUsage diskUsage) {
final int sdkVersion = DataSource.get().getAndroidVersion();
if (sdkVersion < Build.VERSION_CODES.ECLAIR) {
return new MemoryClassDefault();
} else {
return diskUsage.new MemoryClassDetected();
}
}
};
class MemoryClassDetected extends MemoryClass {
@Override
int maxHeap() {
ActivityManager manager = (ActivityManager) DiskUsage.this.getSystemService(Context.ACTIVITY_SERVICE);
return manager.getMemoryClass() * 1024 * 1024;
}
}
MemoryClass memoryClass = MemoryClass.getInstance(this);
private int getMemoryQuota() {
int totalMem = memoryClass.maxHeap();
int numMountPoints = MountPoint.getMountPoints(this).size();
return totalMem / (numMountPoints + 1);
}
static class FileSystemStats {
final int blockSize;
final long freeBlocks;
final long busyBlocks;
final long totalBlocks;
public FileSystemStats(MountPoint mountPoint) {
StatFsSource stats = null;
try {
stats = DataSource.get().statFs(mountPoint.getRoot());
} catch (IllegalArgumentException e) {
Log.e("diskusage",
"Failed to get filesystem stats for " + mountPoint.getRoot(), e);
}
if (stats != null) {
blockSize = stats.getBlockSize();
freeBlocks = stats.getAvailableBlocks();
totalBlocks = stats.getBlockCount();
busyBlocks = totalBlocks - freeBlocks;
} else {
freeBlocks = totalBlocks = busyBlocks = 0;
blockSize = 512;
}
}
public String formatUsageInfo() {
if (totalBlocks == 0) return "Used <no information>";
return String.format("Used %s of %s",
FileSystemEntry.calcSizeString(busyBlocks * blockSize),
FileSystemEntry.calcSizeString(totalBlocks * blockSize));
}
};
public interface ProgressGenerator {
FileSystemEntry lastCreatedFile();
long pos();
};
Runnable makeProgressUpdater(final ProgressGenerator scanner,
final FileSystemStats stats) {
return new Runnable() {
private FileSystemEntry file;
@Override
public void run() {
MyProgressDialog dialog = getPersistantState().loading;
if (dialog != null) {
dialog.setMax(stats.busyBlocks);
FileSystemEntry lastFile = scanner.lastCreatedFile();
if (lastFile != file) {
dialog.setProgress(scanner.pos(), lastFile);
}
file = lastFile;
}
handler.postDelayed(this, 50);
}
};
}
@Override
FileSystemSuperRoot scan() throws IOException, InterruptedException {
MountPoint mountPoint0 = null;
if (key.startsWith("rooted")) {
mountPoint0 = MountPoint.getRooted(this, getRootPath());
} else {
mountPoint0 = MountPoint.getNormal(this, getRootPath());
}
final MountPoint mountPoint = mountPoint0;
final FileSystemStats stats = new FileSystemStats(mountPoint);
int heap = getMemoryQuota();
FileSystemEntry rootElement = null;
final NativeScanner scanner = new NativeScanner(DiskUsage.this, stats.blockSize, stats.busyBlocks, heap);
progressUpdater = makeProgressUpdater(scanner, stats);
handler.post(progressUpdater);
MountPoint realMountPoint = mountPoint;
boolean fakeDataHoneycomb = (isMergedStorage()
&& key.equals("storage:/data"));
if (fakeDataHoneycomb) realMountPoint = MountPoint.getHoneycombSdcard(this);
try {
// if (true) throw new RuntimeException("native fail");
rootElement = scanner.scan(realMountPoint);
} catch (RuntimeException e) {
if (realMountPoint.rootRequired) throw e;
// Legacy code for devices which fail to setup native code
handler.removeCallbacks(progressUpdater);
final Scanner legacyScanner = new Scanner(
20, stats.blockSize, realMountPoint.getExcludeFilter(), stats.busyBlocks, heap);
progressUpdater = makeProgressUpdater(legacyScanner, stats);
handler.post(progressUpdater);
rootElement = legacyScanner.scan(DataSource.get().createLegacyScanFile(realMountPoint.root));
}
handler.removeCallbacks(progressUpdater);
ArrayList<FileSystemEntry> entries = new ArrayList<FileSystemEntry>();
if (rootElement.children != null) {
for (FileSystemEntry e : rootElement.children) {
entries.add(e);
}
}
if (mountPoint.hasApps2SD) {
FileSystemEntry[] apps = loadApps2SD(true, AppFilter.getFilterForDiskUsage(), stats.blockSize);
if (apps != null) {
FileSystemEntry apps2sd =
FileSystemEntry.makeNode(null, "Apps2SD").setChildren(
apps, stats.blockSize);
entries.add(apps2sd);
}
}
if (fakeDataHoneycomb || mountPoint.forceHasApps) {
FileSystemEntry media = FileSystemRoot.makeNode(
"media", realMountPoint.root).setChildren(entries.toArray(new FileSystemEntry[0]),
stats.blockSize);
entries = new ArrayList<FileSystemEntry>();
entries.add(media);
FileSystemEntry[] appList = loadApps2SD(false,
AppFilter.getFilterForHoneycomb(), stats.blockSize);
if (appList != null) {
FileSystemEntry apps = FileSystemEntry.makeNode(null, "Apps").setChildren(appList, stats.blockSize);
entries.add(apps);
}
}
long visibleBlocks = 0;
for (FileSystemEntry e : entries) {
visibleBlocks += e.getSizeInBlocks();
}
long systemBlocks = stats.totalBlocks - stats.freeBlocks - visibleBlocks;
Collections.sort(entries, FileSystemEntry.COMPARE);
if (systemBlocks > 0) {
entries.add(new FileSystemSystemSpace("System data", systemBlocks * stats.blockSize, stats.blockSize));
entries.add(new FileSystemFreeSpace("Free space", stats.freeBlocks * stats.blockSize, stats.blockSize));
} else {
long freeBlocks = stats.freeBlocks + systemBlocks;
if (freeBlocks > 0) {
entries.add(new FileSystemFreeSpace("Free space", freeBlocks * stats.blockSize, stats.blockSize));
}
}
rootElement = FileSystemRoot.makeNode(
getRootTitle(), mountPoint.root)
.setChildren(entries.toArray(new FileSystemEntry[0]), stats.blockSize);
FileSystemSuperRoot newRoot = new FileSystemSuperRoot(stats.blockSize);
newRoot.setChildren(new FileSystemEntry[] { rootElement }, stats.blockSize);
return newRoot;
}
protected FileSystemEntry[] loadApps2SD(boolean sdOnly, AppFilter appFilter, int blockSize) {
final int sdkVersion = DataSource.get().getAndroidVersion();
if (sdkVersion < Build.VERSION_CODES.FROYO && sdOnly) return null;
try {
return (new Apps2SDLoader(this).load(sdOnly, appFilter, blockSize));
} catch (Throwable t) {
Log.e("diskusage", "problem loading apps2sd info", t);
return null;
}
}
public FileSystemEntry.ExcludeFilter getExcludeFilter() {
return MountPoint.getMountPoints(this).get(getRootPath()).getExcludeFilter();
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
this.menu.onPrepareOptionsMenu(menu);
return true;
}
@Override
public String getRootTitle() {
return rootTitle;
}
@Override
public String getRootPath() {
return rootPath;
}
@Override
public String getKey() {
return key;
}
public void searchRequest() {
menu.searchRequest();
}
}