Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions app/src/main/java/com/httrack/android/DirScanner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.httrack.android;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;

/**
* Lists directories for the file chooser off the UI thread. A stat() per entry blows past the ANR
* budget on a large or slow directory. Only the newest scan is drawn, because a slower one started
* earlier would otherwise draw over it.
*/
final class DirScanner {
private static final File ROOT = new File("/");

/** Holds what one scan found. */
static final class Listing {
/** Where the "up one level" entry leads, the filesystem root when there is nothing above. */
final File parent;

/** The visible sub-directories, in the order the filesystem listed them. */
final List<File> subdirectories;

Listing(final File parent, final List<File> subdirectories) {
this.parent = parent;
this.subdirectories = subdirectories;
}
}

/** Takes the listing of the newest scan, on the UI executor. */
interface Sink {
void onListing(Listing listing);
}

private final Executor worker;
private final Executor ui;

/** Counts the scans asked for, so a scan can tell whether it is still the newest. */
private final AtomicInteger newest = new AtomicInteger();

DirScanner(final Executor worker, final Executor ui) {
this.worker = worker;
this.ui = ui;
}

/**
* Scan a directory and hand the result to a sink.
*
* @param dir the directory to list, as the chooser currently has it, never null
* @param fallback listed instead when dir is gone, never null
* @param sink called on the UI executor, unless a later scan has superseded this one
*/
void scan(final File dir, final File fallback, final Sink sink) {
final int ticket = newest.incrementAndGet();
worker.execute(new Runnable() {
@Override
public void run() {
if (ticket != newest.get()) {
return;
}
final Listing listing = list(dir, fallback);
ui.execute(new Runnable() {
@Override
public void run() {
if (ticket == newest.get()) {
sink.onListing(listing);
}
}
});
}
});
}

/**
* Read the parent and the visible sub-directories of a directory. Private, so that the UI thread
* can reach a listing only through scan().
*
* @param dir the directory to list, which may be gone
* @param fallback listed instead when dir is gone, which leaves the parent at the root, because
* a path that no longer exists says nothing about where it sat
* @return what the chooser must show
*/
private static Listing list(final File dir, final File fallback) {
final boolean usable = dir.exists();
return new Listing(usable ? parentOf(dir) : ROOT, subdirectoriesOf(usable ? dir : fallback));
}

/** Returns the directory above dir, or the filesystem root when there is nothing above it. */
private static File parentOf(final File dir) {
final File parent = dir.getParentFile();
return parent != null ? parent : ROOT;
}

/** Returns the sub-directories of dir the chooser offers, which excludes the hidden ones. */
private static List<File> subdirectoriesOf(final File dir) {
final List<File> dirs = new ArrayList<File>();
final File[] list = dir.listFiles();
if (list != null) {
for (final File file : list) {
if (file.isDirectory() && !file.isHidden()) {
dirs.add(file);
}
}
}
return dirs;
}
}
140 changes: 75 additions & 65 deletions app/src/main/java/com/httrack/android/FileChooserActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;

import android.os.Bundle;
import android.os.Handler;
Expand Down Expand Up @@ -33,6 +34,19 @@ public class FileChooserActivity extends Activity implements
// Handler to execute code in UI thread
private final Handler handlerUI = new Handler();

/** Lists the directories to offer on a worker thread, then draws them through handlerUI. */
private final DirScanner scanner = new DirScanner(new Executor() {
@Override
public void execute(final Runnable command) {
new Thread(command).start();
}
}, new Executor() {
@Override
public void execute(final Runnable command) {
handlerUI.post(command);
}
});

/*
* Convert dp (Density-independent Pixels) to px (pixel)
*/
Expand All @@ -51,84 +65,80 @@ private void setFile(final File file) {
}

/*
* Create all tabs for main options menu.
* Ask for the current directory's listing. showDir draws it once it arrives.
*/
private void refreshDir() {
// Clear list
files.clear();
scanner.scan(projectRootFile, defaultRootFile, new DirScanner.Sink() {
@Override
public void onListing(final DirScanner.Listing listing) {
showDir(listing);
}
});
}

// Find parent
final File parent = projectRootFile != null && projectRootFile.exists() ? projectRootFile.getParentFile() : null;
if (parent != null && projectRootFile != null && !parent.getAbsolutePath().equals(projectRootFile.getAbsolutePath())) {
files.add(new Pair<String, File>(getString(R.string.ellipsis), parent));
Log.d("httrack", "found parent of " + projectRootFile.getAbsolutePath() + ": " + parent.getAbsolutePath());
} else {
files.add(new Pair<String, File>(getString(R.string.ellipsis), new File("/")));
Log.d("httrack", "could not find found parent of " + projectRootFile.getAbsolutePath() + ", using /");
/** Draws the listing as one button per entry. */
private void showDir(final DirScanner.Listing listing) {
if (isDestroyed()) {
return;
}

// List directory
final File[] list = projectRootFile != null && projectRootFile.exists() ? projectRootFile
.listFiles() : defaultRootFile.listFiles();
if (list != null) {
for (final File file : list) {
if (file.isDirectory() && !file.isHidden() /* && file.canWrite() */) {
files.add(new Pair<String, File>(file.getName(), file));
}
}
// Clear list
files.clear();

// Up one level, then the sub-directories
files.add(new Pair<String, File>(getString(R.string.ellipsis), listing.parent));
for (final File dir : listing.subdirectories) {
files.add(new Pair<String, File>(dir.getName(), dir));
}

if (files != null) {
// Create tabs
final LinearLayout scroll = LinearLayout.class
.cast(findViewById(R.id.layout));
scroll.removeAllViews();

// Add all buttons linking to options
for (int i = 0; i < files.size(); i++) {
// Add separator
if (borderless && i != 0) {
final View line = new View(this, null, R.style.DividerLineHorizontal);

// FIXME TODO: why in hell isn't my shiny style NOT working ?
final LinearLayout.MarginLayoutParams layout = new LinearLayout.MarginLayoutParams(
LayoutParams.FILL_PARENT, dpToPx(1));
layout.bottomMargin = dpToPx(8);
layout.topMargin = dpToPx(8);
line.setLayoutParams(layout);
line.setBackgroundColor(getResources().getColor(R.color.black));

// Add line
scroll.addView(line);
}
// Create tabs
final LinearLayout scroll = LinearLayout.class
.cast(findViewById(R.id.layout));
scroll.removeAllViews();

// Add all buttons linking to options
for (int i = 0; i < files.size(); i++) {
// Add separator
if (borderless && i != 0) {
final View line = new View(this, null, R.style.DividerLineHorizontal);

// FIXME TODO: why in hell isn't my shiny style NOT working ?
final LinearLayout.MarginLayoutParams layout = new LinearLayout.MarginLayoutParams(
LayoutParams.FILL_PARENT, dpToPx(1));
layout.bottomMargin = dpToPx(8);
layout.topMargin = dpToPx(8);
line.setLayoutParams(layout);
line.setBackgroundColor(getResources().getColor(R.color.black));

// Add line
scroll.addView(line);
}

// Next tab class
final Pair<String, File> entry = files.get(i);
final String title = entry.first;

// Create a flat button ; see
// <http://developer.android.com/guide/topics/ui/controls/button.html#Borderless>
// Added in API level 11
final Button button = borderless ? new Button(this, null,
android.R.attr.borderlessButtonStyle) : new Button(this);
if (borderless) {
// Left-aligned text
button.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
}
// Next tab class
final Pair<String, File> entry = files.get(i);
final String title = entry.first;

// Create a flat button ; see
// <http://developer.android.com/guide/topics/ui/controls/button.html#Borderless>
// Added in API level 11
final Button button = borderless ? new Button(this, null,
android.R.attr.borderlessButtonStyle) : new Button(this);
if (borderless) {
// Left-aligned text
button.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
}

// Title
button.setText(title);
// Title
button.setText(title);

// Set listener
button.setOnClickListener(this);
// Set listener
button.setOnClickListener(this);

// Set tag to our index
button.setTag(i);
// Set tag to our index
button.setTag(i);

// And finally add button
scroll.addView(button);
}
// And finally add button
scroll.addView(button);
}
}

Expand Down
Loading
Loading