/**
* 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 com.google.android.diskusage.datasource.DataSource;
import com.google.android.diskusage.entity.FileSystemEntry;
import com.google.android.diskusage.entity.FileSystemFreeSpace;
import com.google.android.diskusage.entity.FileSystemSuperRoot;
import com.google.android.diskusage.entity.FileSystemSystemSpace;
import com.google.android.diskusage.opengl.FileSystemViewGPU;
import com.google.android.diskusage.opengl.RenderingThread;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Arrays;
public class FileSystemState {
public interface FileSystemView {
/** Does nothing in GPU View. */
public void requestRepaint();
/** Does nothing in GPU View. */
public void requestRepaint(int l, int t, int r, int b);
/** Sends event to wake up rendering thread. */
public void requestRepaintGPU();
/** Post event to main thread from other thread. */
public boolean post(Runnable r);
/** Run action in renderer thread. */
public void runInRenderThread(Runnable r);
public void killRenderThread();
};
static class MainThreadAction {
protected DiskUsage context;
public MainThreadAction(DiskUsage context) {
this.context = context;
}
public void updateTitle(FileSystemEntry position) {
context.setSelectedEntity(position);
}
public void warnOnFileSelect() {
if (VERSION.SDK_INT < VERSION_CODES.HONEYCOMB) {
Toast.makeText(context,
"Press menu to preview or delete", Toast.LENGTH_SHORT).show();
}
}
public void view(FileSystemEntry entry) {
context.view(entry);
}
public void finishOnBack() {
context.finishOnBack();
}
public void searchRequest() {
context.searchRequest();
}
public MainThreadAction indirect() {
return new MainThreadActionIndirect(context);
}
public MainThreadAction direct() {
return new MainThreadAction(context);
}
}
static class MainThreadActionIndirect extends MainThreadAction {
public MainThreadActionIndirect(DiskUsage context) {
super(context);
}
@Override
public void updateTitle(final FileSystemEntry position) {
context.handler.post(new Runnable() {
@Override
public void run() {
context.setSelectedEntity(position);
}
});
}
@Override
public void warnOnFileSelect() {
context.handler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(context,
"Press menu to preview or delete", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void view(final FileSystemEntry entry) {
context.handler.post(new Runnable() {
@Override
public void run() {
context.view(entry);
}
});
}
@Override
public void finishOnBack() {
context.handler.post(new Runnable() {
@Override
public void run() {
context.finishOnBack();
}
});
}
@Override
public void searchRequest() {
context.handler.post(new Runnable() {
@Override
public void run() {
context.searchRequest();
}
});
}
}
private FileSystemView view;
FileSystemSuperRoot masterRoot;
//FileSystemEntry viewRoot;
private Cursor cursor;
// boolean titleNeedUpdate = false;
MainThreadAction mainThreadAction;
private int numSpecialEntries = 0;
private FileSystemFreeSpace freeSpace;
private FileSystemSystemSpace systemSpace;
private long freeSpaceZoom = 0;
private float targetViewDepth;
private long targetViewTop;
private long targetViewBottom;
private int targetElementWidth;
private float prevViewDepth;
private long prevViewTop;
private long prevViewBottom;
private int prevElementWidth;
private float viewDepth;
private long viewTop;
private long viewBottom;
private long displayTop;
private long displayBottom;
private int screenWidth = 400; // Safe values to not crash when touch events
private int screenHeight = 400; // come before screen initialized.
private float yscale;
private long animationStartTime;
private final Interpolator interpolator = new DecelerateInterpolator();
private static long animationDuration = 900;
private static long deletionAnimationDuration = 900;
private float maxLevels = 3.2f;
private boolean fullZoom;
private boolean warnOnFileSelect;
private float touchDepth;
private long touchPoint;
private FileSystemEntry touchEntry;
private float touchX, touchY;
private boolean touchMovement;
private float speedX;
private float speedY;
private long touchZoom;
private int multiNumTouches;
private boolean multitouchReset;
private float touchWidth;
private float touchPointX;
private float minDistance;
private float minDistanceX;
private int minElementWidth;
private int maxElementWidth;
private int stats_num_deletions = 0;
private boolean screenTouching;
public static class VersionedMultitouchHandler {
boolean handleTouch(MyMotionEvent ev) {
return false;
}
private static VersionedMultitouchHandler newInstance(
FileSystemState view) {
final int sdkVersion = DataSource.get().getAndroidVersion();
VersionedMultitouchHandler detector = null;
if (sdkVersion < Build.VERSION_CODES.ECLAIR) {
detector = new VersionedMultitouchHandler();
} else {
detector = view.new MultiTouchHandler();
}
return detector;
}
public MyMotionEvent newMyMotionEvent(MotionEvent ev) {
MyMotionEvent myev = new MyMotionEvent(ev);
setupMulti(ev, myev);
return myev;
}
protected void setupMulti(MotionEvent ev, MyMotionEvent myev) {}
}
private final class MultiTouchHandler extends VersionedMultitouchHandler {
ArrayList<MotionFilter> filterX = new ArrayList<MotionFilter>();
ArrayList<MotionFilter> filterY = new ArrayList<MotionFilter>();
private MotionFilter getFilterX(int i) {
if (filterX.size() <= i)
filterX.add(new MotionFilter());
return filterX.get(i);
}
private MotionFilter getFilterY(int i) {
if (filterY.size() <= i)
filterY.add(new MotionFilter());
return filterY.get(i);
}
@Override
public void setupMulti(MotionEvent ev, MyMotionEvent myev) {
int pointerCount;
float[] xx, yy;
pointerCount = ev.getPointerCount();
xx = new float[pointerCount];
yy = new float[pointerCount];
for (int i = 0; i < pointerCount; i++) {
xx[i] = ev.getX(i);
yy[i] = ev.getY(i);
}
myev.setupMulti(pointerCount, xx, yy);
}
@Override
boolean handleTouch(MyMotionEvent ev) {
int action = ev.getAction();
Integer num = ev.getPointerCount();
if (num == 1) {
return false;
}
// Log.d("diskusage", "multi: " + action + " num = " + num);
if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
// Log.d("diskusage", "multi down");
multitouchReset = true;
for (int i = 0; i < num; i++) {
getFilterX(i).noFilter(ev.getX(i));
getFilterY(i).noFilter(ev.getY(i));
}
}
if (action == MotionEvent.ACTION_MOVE) {
// Log.d("diskusage", "multi move");
float xmin, xmax, ymin, ymax;
ymin = ymax = getFilterX(0).doFilter(ev.getY(0));
xmin = xmax = getFilterY(0).doFilter(ev.getX(0));
for (int i = 1; i < num; i++) {
float x = getFilterX(i).doFilter(ev.getX(i));
float y = getFilterY(i).doFilter(ev.getY(i));
if (x < xmin) xmin = x;
if (x > xmax) xmax = x;
if (y < ymin) ymin = y;
if (y > ymax) ymax = y;
}
if (multitouchReset) {
// Log.d("diskusage", "multi move: reset");
multitouchReset = false;
multiNumTouches = num;
touchMovement = true;
float dy = ymax - ymin;
if (dy < minDistance) dy = minDistance;
float avg_y = 0.5f * (ymax + ymin);
touchZoom = (displayBottom - displayTop) * (long)dy / screenHeight;
touchPoint = displayTop + (displayBottom - displayTop) * (long)avg_y / screenHeight;
float avg_x = 0.5f * (xmax + xmin);
float dx = xmax - xmin;
minDistanceX = FileSystemEntry.elementWidth / 2;
if (dx < minDistanceX) dx = minDistanceX;
touchWidth = dx / FileSystemEntry.elementWidth;
touchPointX = viewDepth + avg_x / FileSystemEntry.elementWidth;
// Log.d("diskusage", "multitouch reset " + avg_x + " : " + dx);
return true;
}
float dy = ymax - ymin;
if (dy < minDistance) dy = minDistance;
long displayBottom_Top = touchZoom * screenHeight / (long) dy;
float avg_y = 0.5f * (ymax + ymin);
displayTop = touchPoint - displayBottom_Top * (long) avg_y / screenHeight;
displayBottom = displayTop + displayBottom_Top;
float avg_x = 0.5f * (xmax + xmin);
float dx = xmax - xmin;
if (dx < minDistanceX) dx = minDistanceX;
FileSystemEntry.elementWidth = (int) (dx / touchWidth);
if (FileSystemEntry.elementWidth < minElementWidth)
FileSystemEntry.elementWidth = minElementWidth;
// else if (FileSystemEntry.elementWidth > maxElementWidth)
// FileSystemEntry.elementWidth = maxElementWidth;
targetElementWidth = FileSystemEntry.elementWidth;
targetViewDepth = viewDepth = touchPointX - avg_x / FileSystemEntry.elementWidth;
maxLevels = screenWidth / (float) FileSystemEntry.elementWidth;
// Log.d("diskusage", "multitouch " + avg_x + " : " + dx + "(" + old_dx + ")");
long dt = (displayBottom - displayTop) / 41;
if (dt < 2) {
displayBottom += 41 * 2;
}
viewTop = displayTop + dt;
viewBottom = displayBottom - dt;
targetViewTop = viewTop;
targetViewBottom = viewBottom;
animationStartTime = 0;
requestRepaint();
return true;
}
return true;
}
};
public VersionedMultitouchHandler multitouchHandler =
VersionedMultitouchHandler.newInstance(this);
public void onMotion(float newTouchX, float newTouchY, long moveTime) {
float touchOffsetX = newTouchX - touchX;
float touchOffsetY = newTouchY - touchY;
speedX += touchOffsetX;
speedY += touchOffsetY;
long dt = moveTime - prevMoveTime;
if (dt > 10) {
speedX *= 10.f / dt;
speedY *= 10.f / dt;
prevMoveTime = moveTime - 10;
}
if (Math.abs(touchOffsetX) < 10 && Math.abs(touchOffsetY)< 10 && !touchMovement)
return;
touchMovement = true;
viewDepth -= touchOffsetX / FileSystemEntry.elementWidth;
if (viewDepth * FileSystemEntry.elementWidth < -screenWidth * 0.6)
viewDepth = -screenWidth * 0.6f / FileSystemEntry.elementWidth;
targetViewDepth = viewDepth;
long offset = (long)(touchOffsetY / yscale);
long allowedOverflow = (long)(screenHeight * 0.6f / yscale);
viewTop -= offset;
viewBottom -= offset;
if (viewTop < -allowedOverflow) {
long oldTop = viewTop;
viewTop = -allowedOverflow;
viewBottom += viewTop - oldTop;
}
if (viewBottom > masterRoot.getSizeForRendering() + allowedOverflow) {
long oldBottom = viewBottom;
viewBottom = masterRoot.getSizeForRendering() + allowedOverflow;
viewTop += viewBottom - oldBottom;
}
targetViewTop = viewTop;
targetViewBottom = viewBottom;
animationStartTime = 0;
touchX = newTouchX;
touchY = newTouchY;
requestRepaint();
return;
}
private static class MotionFilter {
public static float dx = 5;
float cur;
float cur2;
float dx2;;
private float noFilter(float value) {
cur = value;
cur2 = value;
dx2 = 0;
return value;
}
private float doFilter(float val) {
if (val > cur + dx) {
cur += val - (cur + dx);
dx2--;
if (dx2 < 0) dx2 = 0;
} else if (val < cur - dx) {
cur += val - (cur - dx);
dx2--;
if (dx2 < 0) dx2 = 0;
} else {
dx2++;
if (dx2 > dx) dx2 = dx;
}
if (val > cur2 + dx2) {
cur2 += val - (cur2 + dx2);
} else if (val < cur2 - dx2) {
cur2 += val - (cur2 - dx2);
}
return cur2;
}
}
private final MotionFilter filterX = new MotionFilter();
private final MotionFilter filterY = new MotionFilter();
public static class MyMotionEvent {
long eventTime;
float x, y;
float[] xx, yy;
int action;
int pointerCount;
public MyMotionEvent(MotionEvent ev) {
eventTime = ev.getEventTime();
x = ev.getX();
y = ev.getY();
action = ev.getAction();
}
public void setupMulti(int pointerCount, float[] xx, float[] yy) {
this.pointerCount = pointerCount;
this.xx = xx;
this.yy = yy;
}
public float getX() { return x; }
public float getY() { return y; }
public int getAction() { return action; }
public long getEventTime() { return eventTime; }
public Integer getPointerCount() { return pointerCount; }
public float getX(int i) { return xx[i]; }
public float getY(int i) { return yy[i]; }
}
public final boolean onTouchEvent(MyMotionEvent ev) {
try { // finally requestRepaintGPU()
if (sdcardIsEmpty())
return true;
if (deletingEntry != null) {
// setup state of multitouch to reinitialize next time
multiNumTouches = 0;
return true;
}
if (multitouchHandler.handleTouch(ev))
return true;
float newTouchX = ev.getX();
float newTouchY = ev.getY();
int action = ev.getAction();
if (multiNumTouches > 1) {
if (action == MotionEvent.ACTION_UP) {
multiNumTouches = 0;
screenTouching = false;
requestRepaint();
}
return true;
}
if (action == MotionEvent.ACTION_DOWN) {
screenTouching = true;
multiNumTouches = 1;
multitouchReset = true;
newTouchX = filterX.noFilter(newTouchX);
newTouchY = filterY.noFilter(newTouchY);
touchX = newTouchX;
touchY = newTouchY;
touchDepth = (FileSystemEntry.elementWidth * viewDepth + touchX) /
FileSystemEntry.elementWidth;
touchPoint = displayTop + (displayBottom - displayTop) * (long)touchY / screenHeight;
touchEntry = masterRoot.findEntry((int)touchDepth + 1, touchPoint);
if (touchEntry == masterRoot) {
touchEntry = null;
Log.d("diskusage", "warning: masterRoot selected in onTouchEvent");
}
speedX = 0;
speedY = 0;
prevMoveTime = ev.getEventTime();
} else if (action == MotionEvent.ACTION_MOVE) {
long moveTime = ev.getEventTime();
newTouchX = filterX.doFilter(newTouchX);
newTouchY = filterY.doFilter(newTouchY);
onMotion(newTouchX, newTouchY, moveTime);
return true;
} else if (action == MotionEvent.ACTION_UP) {
screenTouching = false;
newTouchX = filterX.doFilter(newTouchX);
newTouchY = filterY.doFilter(newTouchY);
// This prevents first touch after pinch-zoom, removed
// if (multiNumTouches != 1) return true;
if (!touchMovement) {
if (touchEntry == null) {
Log.d("diskusage", "touchEntry == null");
return true;
}
if (masterRoot.depth(touchEntry) > (int)touchDepth + 1) return true;
touchSelect(touchEntry, ev.getEventTime());
return true;
}
touchMovement = false;
{ // copy paste, fling
float touchOffsetX = speedX * 15;
float touchOffsetY = speedY * 15;
targetViewDepth -= touchOffsetX / FileSystemEntry.elementWidth;
if (targetViewDepth * FileSystemEntry.elementWidth < -screenWidth * 0.6)
targetViewDepth = -screenWidth * 0.6f / FileSystemEntry.elementWidth;
long offset = (long)(touchOffsetY / yscale);
long allowedOverflow = (long)(screenHeight * 0.6f / yscale);
targetViewTop -= offset;
targetViewBottom -= offset;
if (targetViewTop < -allowedOverflow) {
long oldTop = targetViewTop;
targetViewTop = -allowedOverflow;
targetViewBottom += targetViewTop - oldTop;
}
if (targetViewBottom > masterRoot.getSizeForRendering() + allowedOverflow) {
long oldBottom = targetViewBottom;
targetViewBottom = masterRoot.getSizeForRendering() + allowedOverflow;
targetViewTop += targetViewBottom - oldBottom;
}
}
if (animationStartTime != 0) return true;
prepareMotion(ev.getEventTime());
animationDuration = 300;
requestRepaint();
}
} finally {
requestRepaintGPU();
}
return true;
}
public FileSystemState(
DiskUsage context, FileSystemSuperRoot root) {
this.mainThreadAction = new MainThreadAction(context);
zoomState = ZoomState.ZOOM_ALLOCATED;
targetViewBottom = root.getSizeForRendering();
//this.viewRoot = root;
this.masterRoot = root;
updateSpecialEntries();
resetCursor();
}
public void resetCursor() {
// FIXME: dirty hacks
cursor = new Cursor(this, masterRoot);
touchEntry = null;
touchMovement = false;
}
private void rescanFinished(FileSystemSuperRoot newRoot) {
masterRoot = newRoot;
updateSpecialEntries();
cursor = new Cursor(this, masterRoot);
requestRepaint();
requestRepaintGPU();
}
public void replaceRootKeepCursor(final FileSystemSuperRoot newRoot,
String searchQuery) {
view.runInRenderThread(new Runnable() {
@Override
public void run() {
FileSystemEntry oldPosition = cursor.position;
FileSystemEntry newPosition =
newRoot.getEntryByName(oldPosition.path2(), false);
if (newPosition == null) newPosition = newRoot.children[0];
int newDepth = newRoot.depth(newPosition);
int oldDepth = masterRoot.depth(cursor.position);
for (; oldDepth > newDepth; oldDepth--) {
oldPosition = oldPosition.parent;
}
long oldTop = masterRoot.getOffset(oldPosition);
long oldSize = oldPosition.getSizeForRendering();
long oldBottom = oldTop + oldSize;
long newTop = newRoot.getOffset(newPosition);
long newSize = newPosition.getSizeForRendering();
long newBottom = newTop + newSize;
double above = (oldTop - targetViewTop) / (double)oldSize;
double bellow = (targetViewBottom - oldBottom) / (double)oldSize;
long newViewTop = newTop - (long)(above * newSize);
long newViewBottom = (long)(bellow * newSize) + newBottom;
prepareMotion(SystemClock.uptimeMillis());
targetViewTop = viewTop = newViewTop;
targetViewBottom = viewBottom = newViewBottom;
if (targetViewTop > newTop) targetViewTop = newTop;
if (targetViewBottom < newBottom) targetViewBottom = newBottom;
animationDuration = 300;
rescanFinished(newRoot);
cursor.set(FileSystemState.this, newPosition);
}
});
}
public void startZoomAnimationInRenderThread(final FileSystemSuperRoot newRoot,
final boolean animate, final boolean keepCursor) {
view.runInRenderThread(new Runnable() {
@Override
public void run() {
if (newRoot != null) rescanFinished(newRoot);
if (animate) {
long large = masterRoot.getSizeForRendering() * 10;
long center = masterRoot.getSizeForRendering() / 2;
viewTop = center - large;
viewBottom = center + large;
viewDepth = 0;
prepareMotion(SystemClock.uptimeMillis());
animationDuration = 300;
targetViewTop = 0;
targetViewBottom = masterRoot.getSizeForRendering();
targetViewDepth = 0;
zoomState = ZoomState.ZOOM_ALLOCATED;
setZoomState();
}
}
});
}
public void defaultZoom() {
zoomState = ZoomState.ZOOM_ALLOCATED;
setZoomState();
}
public void setView(FileSystemView view) {
this.view = view;
if (view instanceof FileSystemViewGPU) {
mainThreadAction = mainThreadAction.indirect();
} else {
mainThreadAction = mainThreadAction.direct();
}
}
private void updateSpecialEntries() {
numSpecialEntries = 0;
freeSpace = null;
systemSpace = null;
freeSpaceZoom = 0;
if (masterRoot.children == null) return;
FileSystemEntry root = masterRoot.children[0];
if (root.children == null) return;
for (FileSystemEntry e : root.children) {
if (e instanceof FileSystemSystemSpace) {
systemSpace = (FileSystemSystemSpace) e;
numSpecialEntries++;
}
if (e instanceof FileSystemFreeSpace) {
numSpecialEntries++;
freeSpace = (FileSystemFreeSpace) e;
}
}
}
private final boolean preDraw() {
fadeAwayEntry();
boolean animation = deletingEntry != null;
long curr = SystemClock.uptimeMillis();
if (curr > animationStartTime + animationDuration) {
// no animation
viewTop = targetViewTop;
viewBottom = targetViewBottom;
viewDepth = targetViewDepth;
FileSystemEntry.elementWidth = targetElementWidth;
maxLevels = screenWidth / (float) targetElementWidth;
} else {
double f = interpolator.getInterpolation((curr - animationStartTime) / (float) animationDuration);
viewTop = (long)(f * targetViewTop + (1-f) * prevViewTop);
viewBottom = (long)(f * targetViewBottom + (1-f) * prevViewBottom);
viewDepth = (float)(f * targetViewDepth + (1-f) * prevViewDepth);
FileSystemEntry.elementWidth = (int)(f * targetElementWidth + (1-f) * prevElementWidth);
animation = true;
}
long dt = (viewBottom - viewTop) / 40;
displayTop = viewTop - dt;
displayBottom = viewBottom + dt;
yscale = screenHeight / (float)(displayBottom - displayTop);
return animation;
}
private final boolean postDraw(boolean animation) {
boolean needRepaint = false;
if (animation) {
// view.requestRepaint();
return true;
} else if (!screenTouching) {
if (targetViewTop < 0 || targetViewBottom > masterRoot.getSizeForRendering()
|| viewDepth < 0 || FileSystemEntry.elementWidth > maxElementWidth) {
prepareMotion(SystemClock.uptimeMillis());
animationDuration = 300;
// view.requestRepaint();
needRepaint = true;
if (targetViewTop < 0) {
long oldTop = targetViewTop;
targetViewTop = 0;
targetViewBottom += targetViewTop - oldTop;
} else if (targetViewBottom > masterRoot.getSizeForRendering()) {
long oldBottom = targetViewBottom;
targetViewBottom = masterRoot.getSizeForRendering();
targetViewTop += targetViewBottom - oldBottom;
}
if (targetViewTop < 0) {
targetViewTop = 0;
}
if (targetViewBottom > masterRoot.getSizeForRendering()) {
targetViewBottom = masterRoot.getSizeForRendering();
}
if (viewDepth < 0) {
targetViewDepth = 0;
}
if (targetElementWidth > maxElementWidth) {
targetElementWidth = maxElementWidth;
}
}
}
return needRepaint;
}
private final void paintSlowGPU(final RenderingThread rt,
long viewTop, long viewBottom, float viewDepth, int screenWidth, int screenHeight) {
Rect bounds2 = new Rect(0, 0, screenWidth, screenHeight);
masterRoot.paintGPU(rt, bounds2, cursor, viewTop, viewDepth, yscale, screenHeight, numSpecialEntries);
}
public boolean onDrawGPU(RenderingThread rt) {
// Log.d("diskusage", "drawFrame (pre) viewTop = " + viewTop + " viewBottom = " + viewBottom);
try {
boolean animation = preDraw();
// Log.d("diskusage", "drawFrame viewTop = " + viewTop + " viewBottom = " + viewBottom);
paintSlowGPU(rt, displayTop, displayBottom, viewDepth, screenWidth, screenHeight);
return postDraw(animation);
} catch (Throwable t) {
Log.d("DiskUsage", "Got exception", t);
}
return false;
}
private final void paintSlow(final Canvas canvas,
long viewTop, long viewBottom, float viewDepth, Rect bounds, int screenHeight) {
if (bounds.bottom != 0 || bounds.top != 0 || bounds.left != 0 || bounds.right != 0) {
masterRoot.paint(canvas, bounds, cursor, viewTop, viewDepth, yscale, screenHeight, numSpecialEntries);
} else {
Rect bounds2 = new Rect(0, 0, screenWidth, screenHeight);
masterRoot.paint(canvas, bounds2, cursor, viewTop, viewDepth, yscale, screenHeight, numSpecialEntries);
}
}
public final void onDraw2(final Canvas canvas) {
try {
boolean animation = preDraw();
Rect bounds = canvas.getClipBounds();
paintSlow(canvas, displayTop, displayBottom, viewDepth, bounds, screenHeight);
boolean needRepaint = postDraw(animation);
if (needRepaint) {
requestRepaint();
}
} catch (Throwable t) {
Log.d("DiskUsage", "Got exception", t);
}
}
public final void prepareMotion(long time) {
// Log.d("diskusage", "prepare motion");
animationDuration = 900;
prevViewDepth = viewDepth;
prevViewTop = viewTop;
prevViewBottom = viewBottom;
prevElementWidth = FileSystemEntry.elementWidth;
animationStartTime = time;
}
final void invalidate(Cursor cursor) {
float cursorx0 = (cursor.depth - viewDepth) * FileSystemEntry.elementWidth;
float cursory0 = (cursor.top - displayTop) * yscale;
float cursorx1 = cursorx0 + FileSystemEntry.elementWidth;
float cursory1 = cursory0 + cursor.position.getSizeForRendering() * yscale;
requestRepaint((int)cursorx0, (int)cursory0, (int)cursorx1 + 2, (int)cursory1 + 2);
}
long prevMoveTime;
/*
* TODO:
* Add Message to the screen in DeleteActivity
* Check that DeleteActivity has right title
* multitouch on eclair
* Fling works bad on eclair, use 10ms approximation for last movement
*/
private void touchSelect(FileSystemEntry entry, long eventTime) {
FileSystemEntry prevCursor = cursor.position;
int prevDepth = cursor.depth;
cursor.set(this, entry);
int currDepth = cursor.depth;
prepareMotion(eventTime);
if ((entry == masterRoot.children[0]) || (entry instanceof FileSystemFreeSpace)) {
// Log.d("diskusage", "special case for " + entry.name);
toggleZoomState();
return;
}
zoomState = ZoomState.ZOOM_OTHER;
zoomFitLabelMoveUp(eventTime);
zoomFitToScreen(eventTime);
boolean has_children = entry.children != null && entry.children.length != 0;
if (!has_children) {
// Log.d("diskusage", "zoom file");
fullZoom = false;
if (targetViewTop == prevViewTop
&& targetViewBottom == prevViewBottom) {
if ((!warnOnFileSelect) && (!(entry instanceof FileSystemSystemSpace))) {
mainThreadAction.warnOnFileSelect();
warnOnFileSelect = true;
}
}
float minRequiredDepth = cursor.depth + 1 + (has_children ? 1 : 0) - maxLevels;
if (targetViewDepth < minRequiredDepth) {
targetViewDepth = minRequiredDepth;
}
return;
} else if (prevCursor == entry) {
// Log.d("diskusage", "zoom toggle same element");
fullZoom = !fullZoom;
} else if (currDepth < prevDepth) {
// Log.d("diskusage", "zoom false");
fullZoom = false;
} else {
if (entry.getSizeForRendering() * yscale > FileSystemEntry.fontSize * 2) {
fullZoom = true;
} else {
fullZoom = false;
}
}
float maxRequiredDepth = cursor.depth - (cursor.depth > 0 ? 1 : 0);
float minRequiredDepth = cursor.depth + 1 + (has_children ? 1 : 0) - maxLevels;
if (minRequiredDepth > maxRequiredDepth) {
// Log.d("diskusage", "zoom levels overlap, fullZoom = " + fullZoom);
if (fullZoom) {
maxRequiredDepth = minRequiredDepth;
} else {
minRequiredDepth = maxRequiredDepth;
}
}
if (targetViewDepth < minRequiredDepth) {
targetViewDepth = minRequiredDepth;
} else if (targetViewDepth > maxRequiredDepth) {
targetViewDepth = maxRequiredDepth;
}
if (fullZoom) {
targetViewTop = cursor.top;
targetViewBottom = cursor.top + cursor.position.getSizeForRendering();
} else {
}
if (targetViewBottom == prevViewBottom && targetViewTop == prevViewTop) {
fullZoom = false;
targetViewTop = cursor.top + 1;
targetViewBottom = cursor.top + cursor.position.getSizeForRendering() - 1;
zoomFitLabelMoveUp(eventTime);
zoomFitToScreen(eventTime);
}
long freeSpaceClip = getFreeSpaceZoom();
if (targetViewBottom > freeSpaceClip) {
targetViewBottom = freeSpaceClip;
if (targetViewTop == 0) zoomState = ZoomState.ZOOM_ALLOCATED;
}
}
private final void zoomFitLabel(long eventTime) {
if (cursor.position.getSizeForRendering() == 0) {
//Log.d("DiskUsage", "position is of zero size");
return;
}
float yscale = screenHeight / (float)(targetViewBottom - targetViewTop);
if (cursor.position.getSizeForRendering() * yscale > FileSystemEntry.fontSize * 2 + 2) {
//Log.d("DiskUsage", "position large enough to contain label");
} else {
//Log.d("DiskUsage", "zoom in");
float new_yscale = FileSystemEntry.fontSize * 2.5f / cursor.position.getSizeForRendering();
prepareMotion(eventTime);
targetViewTop = targetViewBottom - (long) (screenHeight / new_yscale);
if (targetViewTop > cursor.top) {
//Log.d("DiskUsage", "moving down to fit view after zoom in");
// 10% from top
long offset = cursor.top - (long)(targetViewTop * 0.8 + targetViewBottom * 0.2);
targetViewTop += offset;
targetViewBottom += offset;
if (targetViewTop < 0) {
//Log.d("DiskUsage", "at the top");
targetViewBottom -= targetViewTop;
targetViewTop = 0;
}
}
}
}
private final void zoomFitLabelMoveUp(long eventTime) {
if (cursor.position.getSizeForRendering() == 0) {
//Log.d("DiskUsage", "position is of zero size");
return;
}
zoomFitLabel(eventTime);
if (targetViewBottom < cursor.top + cursor.position.getSizeForRendering()) {
//Log.d("DiskUsage", "move up as needed");
prepareMotion(eventTime);
long offset = cursor.top + cursor.position.getSizeForRendering()
- (long)(targetViewTop * 0.2 + targetViewBottom * 0.8);
targetViewTop += offset;
targetViewBottom += offset;
if (targetViewBottom > masterRoot.getSizeForRendering()) {
long diff = targetViewBottom - masterRoot.getSizeForRendering();
targetViewBottom = masterRoot.getSizeForRendering();
targetViewTop -= diff;
}
}
requestRepaint();
}
private final void zoomFitToScreen(long eventTime) {
if (targetViewTop < cursor.top &&
targetViewBottom > cursor.top + cursor.position.getSizeForRendering()) {
// Log.d("DiskUsage", "fits in, no need for zoom out");
return;
}
//Log.d("DiskUsage", "zoom out");
prepareMotion(eventTime);
FileSystemEntry viewRoot = cursor.position.parent;
targetViewTop = masterRoot.getOffset(viewRoot);
long size = viewRoot.getSizeForRendering();
targetViewBottom = targetViewTop + size;
zoomFitLabelMoveUp(eventTime);
requestRepaint();
}
private final boolean back(long eventTime) {
FileSystemEntry newpos = cursor.position.parent;
if (newpos == masterRoot) {
return false;
}
cursor.set(this, newpos);
if (masterRoot.children != null && newpos == masterRoot.children[0]) {
prepareMotion(eventTime);
zoomState = ZoomState.ZOOM_FULL;
setZoomState();
return true;
}
int requiredDepth = cursor.depth - (cursor.position.parent == masterRoot ? 0 : 1);
if (targetViewDepth > requiredDepth) {
prepareMotion(eventTime);
targetViewDepth = requiredDepth;
}
zoomFitToScreen(eventTime);
return true;
}
public boolean isGPU() {
return view instanceof FileSystemViewGPU;
}
private final void moveAwayCursor(FileSystemEntry entry) {
if (cursor.position != entry) return;
// FIXME: should not be needed
// view.requestRepaint();
// cursor.set(this, entry);
try {
cursor.up(this);
} catch (RuntimeException e) {
// getPrev -> getIndexOf() can sometimes when this called from moveAwayCursor()
}
if (cursor.position != entry) {
return;
}
cursor.left(this);
// if (cursor.position == entry) {
// cursor.position = masterRoot.children[0];
// }
}
public final void removeInRenderThread(final FileSystemEntry entry) {
view.runInRenderThread(new Runnable() {
@Override
public void run() {
stats_num_deletions++;
fadeAwayEntryStart(entry, FileSystemState.this);
}
});
requestRepaintGPU();
requestRepaint();
}
private FileSystemEntry deletingEntry = null;
private long deletingAnimationStartTime = 0;
private long deletingInitialSize;
private final void deleteDeletingEntry() {
if (deletingEntry.parent == masterRoot) {
throw new RuntimeException("sdcard deletion is not available in UI");
}
int displayBlockSize = masterRoot.getDisplayBlockSize();
moveAwayCursor(deletingEntry);
deletingEntry.remove(displayBlockSize);
long deletingEntryBlocks = deletingEntry.getSizeInBlocks();
if (freeSpace != null) {
freeSpace.setSizeInBlocks(freeSpace.getSizeInBlocks() + deletingEntryBlocks, displayBlockSize);
masterRoot.setSizeInBlocks(masterRoot.getSizeInBlocks() + deletingEntryBlocks, displayBlockSize);
masterRoot.children[0].setSizeInBlocks(masterRoot.children[0].getSizeInBlocks()
+ deletingEntryBlocks, displayBlockSize);
freeSpace.clearDrawingCache();
}
FileSystemEntry.deletedEntry = null;
FileSystemEntry parent = deletingEntry.parent;
long freeSpaceEncoded = 0, systemSpaceEncoded = 0;
if (freeSpace != null) {
freeSpaceEncoded = freeSpace.encodedSize;
freeSpace.encodedSize = -2;
}
if (systemSpace != null) {
systemSpaceEncoded = systemSpace.encodedSize;
systemSpace.encodedSize = -1;
}
// Sort elements otherwise painting code works incorrect
while (parent != null) {
Arrays.sort(parent.children, FileSystemEntry.COMPARE);
parent = parent.parent;
}
for (FileSystemEntry e : masterRoot.children[0].children) {
Log.d("diskusage", "entry = " + e.name + " " + e.getSizeInBlocks());
}
if (freeSpace != null) {
freeSpace.encodedSize = freeSpaceEncoded;
}
if (systemSpace != null) {
systemSpace.encodedSize = systemSpaceEncoded;
}
deletingEntry = null;
cursor.set(this, cursor.position);
}
private final void fadeAwayEntryStart(FileSystemEntry entry, FileSystemState view) {
if (deletingEntry != null) {
deleteDeletingEntry();
}
deletingAnimationStartTime = 0;
deletingEntry = entry;
FileSystemEntry.deletedEntry = entry;
deletingInitialSize = entry.getSizeInBlocks();
}
// Should be called from main thread
void requestRepaint() {
// Does nothing in GPU View
view.requestRepaint();
}
// Should be called from main thread
private void requestRepaint(int l, int t, int r, int b) {
// Does nothing in GPU View
view.requestRepaint(l, t, r, b);
}
// Should be called from main thread
void requestRepaintGPU() {
// Only for GPU View
view.requestRepaintGPU();
}
// *** Called from different threads ***
void post(Runnable r) {
view.post(r);
}
private final void fadeAwayEntry() {
FileSystemEntry entry = deletingEntry;
if (entry == null) return;
// Log.d("diskusage", "deletion in progress");
long time = SystemClock.uptimeMillis();
if (deletingAnimationStartTime == 0) {
deletingAnimationStartTime = time;
}
long dt = time - deletingAnimationStartTime;
// Log.d("diskusage", "dt = + " + dt);
if (dt > deletionAnimationDuration) {
deleteDeletingEntry();
return;
}
requestRepaint();
float f = interpolator.getInterpolation(dt / (float) animationDuration);
// Log.d("diskusage", "f = + " + f);
long prevSize = entry.getSizeInBlocks();
long newBlocks = (long)((1 - f) * deletingInitialSize);
// Log.d("diskusage", "newSize = + " + newSize);
long dSize = newBlocks - prevSize;
if (dSize >= 0) return;
FileSystemEntry parent = entry.parent;
int displayBlockSize = masterRoot.getDisplayBlockSize();
while (parent != null) {
parent.setSizeInBlocks(parent.getSizeInBlocks() + dSize, displayBlockSize);
parent = parent.parent;
}
if (freeSpace != null) {
masterRoot.setSizeInBlocks(masterRoot.getSizeInBlocks() - dSize, displayBlockSize);
masterRoot.children[0].setSizeInBlocks(masterRoot.children[0].getSizeInBlocks()
- dSize, displayBlockSize);
freeSpace.setSizeInBlocks(freeSpace.getSizeInBlocks() - dSize, displayBlockSize);
}
// truncate children
while (true) {
long deltaBlocks = newBlocks - entry.getSizeInBlocks();
if (deltaBlocks == 0) return;
entry.setSizeInBlocks(entry.getSizeInBlocks() + deltaBlocks, displayBlockSize);
if (entry.children == null || entry.children.length == 0)
return;
FileSystemEntry[] children = entry.children;
long blocks = 0;
FileSystemEntry prevEntry = entry;
for (int i = 0; i < children.length; i++) {
blocks += children[i].getSizeInBlocks();
// if sum of sizes of children less then newSize continue
if (newBlocks > blocks) continue;
// size of children larger than newSize, need to trunc last child
long lastChildSizeChange = blocks - newBlocks;
long newChildSize = children[i].getSizeInBlocks() - lastChildSizeChange;
// size of last child will be updated at the begining of while loop
newBlocks = newChildSize;
FileSystemEntry[] newChildren = new FileSystemEntry[i + 1];
System.arraycopy(children, 0, newChildren, 0, i + 1);
entry.children = newChildren;
entry = children[i];
break;
}
if (prevEntry == entry) {
// Entry was truncated, but not its children
break;
}
}
}
public final void restore(FileSystemEntry entry) {
view.runInRenderThread(new Runnable() {
@Override
public void run() {
if (deletingEntry != null) {
deleteDeletingEntry();
}
}
});
}
public final boolean sdcardIsEmpty() {
return cursor.position == masterRoot;
}
public final boolean onKeyDown(final int keyCode, final KeyEvent event) {
if (sdcardIsEmpty())
return false;
try { // finally requestRepaintGPU()
if (keyCode == KeyEvent.KEYCODE_SEARCH) {
mainThreadAction.searchRequest();
return true;
}
if (keyCode == KeyEvent.KEYCODE_BACK) {
mainThreadAction.finishOnBack();
return true;
}
if (deletingEntry != null) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_DOWN:
return true;
}
return false;
}
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
cursor.down(this);
zoomFitLabelMoveUp(event.getEventTime());
zoomFitToScreen(event.getEventTime());
return true;
}
if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
cursor.up(this);
zoomFitLabel(event.getEventTime());
zoomFitToScreen(event.getEventTime());
return true;
}
if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
back(event.getEventTime());
return true;
}
if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
cursor.right(this);
zoomFitLabelMoveUp(event.getEventTime());
float requiredDepth = cursor.depth + 1 + (cursor.position.children == null ? 0 : 1) - maxLevels;
if (viewDepth < requiredDepth) {
prepareMotion(event.getEventTime());
targetViewDepth = requiredDepth;
}
return true;
}
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
final FileSystemEntry selected = cursor.position;
// FIXME: hack to disable removal of /sdcard
if (selected == masterRoot.children[0]) return true;
mainThreadAction.view(selected);
}
/*if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ENTER)) {
return back();
}*/
//Log.d("DiskUsage", "Key down = " + keyCode + " " + event);
} finally {
requestRepaintGPU();
}
return false;
}
// FIXME: can be called from different thread
public final void layout(
boolean changed, int left, int top, int right,
int bottom, int width, int height) {
screenWidth = width;
screenHeight = height;
minElementWidth = screenWidth / 8;
maxElementWidth = screenWidth / 2;
// FIXME: may be too large
MotionFilter.dx = (screenHeight + screenWidth) / 50;
minDistance = screenHeight > screenWidth ? screenHeight / 10 : screenWidth / 10;
Log.d("diskusage", "screen = " + screenWidth + "x" + screenHeight);
FileSystemEntry.elementWidth = targetElementWidth = (int) (screenWidth / maxLevels);
setZoomState();
}
public void restoreStateInRenderThread(final Bundle inState) {
view.runInRenderThread(new Runnable() {
@Override
public void run() {
String cursorName = inState.getString("cursor");
if (cursorName == null) return;
FileSystemEntry entry = masterRoot.getEntryByName(cursorName, true);
if (entry == null) return;
cursor.set(FileSystemState.this, entry);
targetViewDepth = prevViewDepth = viewDepth = inState.getFloat("viewDepth");
targetViewTop = prevViewTop = viewTop = inState.getLong("viewTop");
targetViewBottom = prevViewBottom = viewBottom = inState.getLong("viewBottom");
switch (inState.getInt("zoomState")) {
case 0: zoomState = ZoomState.ZOOM_ALLOCATED; break;
case 1: zoomState = ZoomState.ZOOM_FULL; break;
default: zoomState = ZoomState.ZOOM_OTHER; break;
}
maxLevels = inState.getFloat("maxLevels");
}
});
}
public void selectFileInRendererThread(final String path) {
view.runInRenderThread(new Runnable() {
@Override
public void run() {
FileSystemEntry e = masterRoot.getByAbsolutePath(path);
if (e != null) {
touchSelect(e, 50);
touchSelect(e, 5000);
}
}
});
}
public void saveState(Bundle outState) {
outState.putString("cursor", cursor.position.path2());
outState.putFloat("viewDepth", viewDepth);
outState.putLong("viewTop", viewTop);
outState.putLong("viewBottom", viewBottom);
outState.putFloat("maxLevels", maxLevels);
outState.putInt("zoomState",
zoomState == ZoomState.ZOOM_ALLOCATED ? 0 : (
zoomState == ZoomState.ZOOM_FULL ? 1 : 2));
}
private long getFreeSpaceZoom() {
if (freeSpaceZoom != 0) return freeSpaceZoom;
if (freeSpace == null) return masterRoot.getSizeForRendering();
freeSpaceZoom = masterRoot.getSizeForRendering();
long busy = masterRoot.getSizeForRendering() - freeSpace.getSizeForRendering();
float message = FileSystemEntry.fontSize * 2 + 1f;
float height = screenHeight / 41f * 40f;
long required = (long)(busy * (height / (height - message)));
required *= 40f / 40.5f;
if (required < freeSpaceZoom * 0.9f)
freeSpaceZoom = required;
return freeSpaceZoom;
}
private void setZoomState() {
if (screenHeight == 0) return;
if (zoomState == ZoomState.ZOOM_ALLOCATED) {
targetViewDepth = 0;
targetViewTop = 0;
targetViewBottom = getFreeSpaceZoom();
} else if (zoomState == ZoomState.ZOOM_FULL) {
targetViewDepth = 0;
targetViewTop = 0;
targetViewBottom = masterRoot.getSizeForRendering();
}
}
private void toggleZoomState() {
zoomState = (zoomState == ZoomState.ZOOM_ALLOCATED) ?
ZoomState.ZOOM_FULL : ZoomState.ZOOM_ALLOCATED;
setZoomState();
}
private enum ZoomState {
ZOOM_FULL,
ZOOM_ALLOCATED,
ZOOM_OTHER
};
private ZoomState zoomState = ZoomState.ZOOM_OTHER;
public void killRenderThread() {
view.killRenderThread();
}
public void draw300ms() {
long curr = SystemClock.uptimeMillis();
if (curr > animationStartTime + animationDuration) {
viewTop = targetViewTop;
viewBottom = targetViewBottom;
prepareMotion(SystemClock.uptimeMillis());
animationDuration = 300;
}
}
}