changes for NoNet-Release

add project converted to Android Studio
This commit is contained in:
Philipp Crocoll 2016-08-20 20:47:52 +02:00
parent a1c1c142a1
commit 84a1dac535
502 changed files with 18699 additions and 19 deletions

View File

@ -104,8 +104,8 @@
<orderEntry type="library" exported="" name="httpcore-4.0.1" level="project" />
<orderEntry type="library" exported="" name="json_simple-1.1" level="project" />
<orderEntry type="library" exported="" name="google-http-client-android-1.16.0-rc" level="project" />
<orderEntry type="library" exported="" name="google-http-client-gson-1.20.0" level="project" />
<orderEntry type="library" exported="" name="gson-2.1" level="project" />
<orderEntry type="library" exported="" name="google-http-client-gson-1.20.0" level="project" />
<orderEntry type="library" exported="" name="google-http-client-jackson-1.16.0-rc" level="project" />
<orderEntry type="library" exported="" name="commons-logging-1.1.1" level="project" />
</component>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

View File

@ -5,7 +5,7 @@
<GradleProjectSettings>
<option name="distributionType" value="LOCAL" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="C:\Program Files\Android\Android Studio\gradle\gradle-2.2.1" />
<option name="gradleHome" value="C:\Program Files\Android\Android Studio1\gradle\gradle-2.10" />
<option name="gradleJvm" value="1.7" />
<option name="modules">
<set>
@ -13,6 +13,12 @@
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="myModules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

File diff suppressed because it is too large Load Diff

View File

@ -12,10 +12,8 @@
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
<option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugAndroidTest" />
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileDebugAndroidTestSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateDebugAndroidTestSources" />
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--Generated by crowdin.com-->
<resources>
<string name="open_entry">ورودی را انتخاب کنید</string>
<string name="kp2a_user">کاربر</string>
<string name="kp2a_password">کلمه عبور</string>
<string name="kp2a_auto_fill">پر کردن خودکار فعال شد</string>
</resources>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--Generated by crowdin.com-->
<resources>
<string name="change_entry">Seleccionar outra entrada</string>
<string name="open_entry">Seleccionar entrada</string>
<string name="kp2a_user">Usuario</string>
<string name="kp2a_password">Contrasinal</string>
<string name="kp2a_simple_keyboard">Teclado simple</string>
<string name="kp2a_lock_on_sendgodone">Bloquear a base de datos ao rematar</string>
<string name="kp2a_switch_on_sendgodone">Cambiar de teclado ao rematar</string>
</resources>

View File

@ -0,0 +1 @@
code

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
<entry name="!?*.aj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>

View File

@ -0,0 +1,3 @@
<component name="CopyrightManager">
<settings default="" />
</component>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="LOCAL" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="C:\Program Files\Android\Android Studio\gradle\gradle-2.2.1" />
<option name="gradleJvm" value="1.7" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -0,0 +1,11 @@
<component name="libraryTable">
<library name="support-v4-18.0.0">
<CLASSES>
<root url="jar://$USER_HOME$/AppData/Local/Android/android-sdk/extras/android/m2repository/com/android/support/support-v4/18.0.0/support-v4-18.0.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$USER_HOME$/AppData/Local/Android/android-sdk/extras/android/m2repository/com/android/support/support-v4/18.0.0/support-v4-18.0.0-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<entry_points version="2.0" />
</component>
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
<OptionsSetting value="true" id="Add" />
<OptionsSetting value="true" id="Remove" />
<OptionsSetting value="true" id="Checkout" />
<OptionsSetting value="true" id="Update" />
<OptionsSetting value="true" id="Status" />
<OptionsSetting value="true" id="Edit" />
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.7" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
<component name="masterDetails">
<states>
<state key="ProjectJDKs.UI">
<settings>
<last-edited>1.7</last-edited>
<splitter-proportions>
<option name="proportions">
<list>
<option value="0.2" />
</list>
</option>
</splitter-proportions>
</settings>
</state>
</states>
</component>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/android-filechooser-AS.iml" filepath="$PROJECT_DIR$/android-filechooser-AS.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
</modules>
</component>
</project>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="android-filechooser-AS" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="java-gradle" name="Java-Gradle">
<configuration>
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
<option name="BUILDABLE" value="false" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
</content>
<orderEntry type="jdk" jdkName="1.7" jdkType="JavaSDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="android-filechooser-AS" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":app" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="debug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
<option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugAndroidTest" />
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileDebugAndroidTestSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateDebugAndroidTestSources" />
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
<option name="LIBRARY_PROJECT" value="true" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/androidTest/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="support-v4-18.0.0" level="project" />
</component>
</module>

View File

@ -0,0 +1,22 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion 23
buildToolsVersion "23.0.0"
defaultConfig {
minSdkVersion 15
targetSdkVersion 15
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
}
dependencies {
compile 'com.android.support:support-v4:18.0.0'
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (c) 2012 Hai Bison
See the file LICENSE at the root directory of this project for copying
permission.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="group.pals.android.lib.ui.filechooser" >
<uses-sdk
android:minSdkVersion="15"
android:targetSdkVersion="23" />
</manifest>

View File

@ -0,0 +1,548 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser;
import group.pals.android.lib.ui.filechooser.prefs.DisplayPrefs;
import group.pals.android.lib.ui.filechooser.prefs.DisplayPrefs.FileTimeDisplay;
import group.pals.android.lib.ui.filechooser.providers.BaseFileProviderUtils;
import group.pals.android.lib.ui.filechooser.providers.basefile.BaseFileContract.BaseFile;
import group.pals.android.lib.ui.filechooser.utils.Converter;
import group.pals.android.lib.ui.filechooser.utils.DateUtils;
import group.pals.android.lib.ui.filechooser.utils.Utils;
import group.pals.android.lib.ui.filechooser.utils.ui.ContextMenuUtils;
import group.pals.android.lib.ui.filechooser.utils.ui.LoadingDialog;
import group.pals.android.lib.ui.filechooser.utils.ui.Ui;
import java.util.ArrayList;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.support.v4.widget.ResourceCursorAdapter;
import android.util.Log;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;
/**
* Adapter of base file.
*
* @author Hai Bison
*
*/
public class BaseFileAdapter extends ResourceCursorAdapter {
/**
* Used for debugging...
*/
private static final String CLASSNAME = BaseFileAdapter.class.getName();
/**
* Listener for building context menu editor.
*
* @author Hai Bison
* @since v5.1 beta
*/
public static interface OnBuildOptionsMenuListener {
/**
* Will be called after the user touched on the icon of the item.
*
* @param view
* the view displaying the item.
* @param cursor
* the item which its icon has been touched.
*/
void onBuildOptionsMenu(View view, Cursor cursor);
/**
* Will be called after the user touched and held ("long click") on the
* icon of the item.
*
* @param view
* the view displaying the item.
* @param cursor
* the item which its icon has been touched.
*/
void onBuildAdvancedOptionsMenu(View view, Cursor cursor);
}// OnBuildOptionsMenuListener
private final int mFilterMode;
private final FileTimeDisplay mFileTimeDisplay;
private final Integer[] mAdvancedSelectionOptions;
private boolean mMultiSelection;
private OnBuildOptionsMenuListener mOnBuildOptionsMenuListener;
public BaseFileAdapter(Context context, int filterMode,
boolean multiSelection) {
super(context, R.layout.afc_file_item, null, 0);
mFilterMode = filterMode;
mMultiSelection = multiSelection;
switch (mFilterMode) {
case BaseFile.FILTER_FILES_AND_DIRECTORIES:
mAdvancedSelectionOptions = new Integer[] {
R.string.afc_cmd_advanced_selection_all,
R.string.afc_cmd_advanced_selection_none,
R.string.afc_cmd_advanced_selection_invert,
R.string.afc_cmd_select_all_files,
R.string.afc_cmd_select_all_folders };
break;// FILTER_FILES_AND_DIRECTORIES
default:
mAdvancedSelectionOptions = new Integer[] {
R.string.afc_cmd_advanced_selection_all,
R.string.afc_cmd_advanced_selection_none,
R.string.afc_cmd_advanced_selection_invert };
break;// FILTER_DIRECTORIES_ONLY and FILTER_FILES_ONLY
}
mFileTimeDisplay = new FileTimeDisplay(
DisplayPrefs.isShowTimeForOldDaysThisYear(context),
DisplayPrefs.isShowTimeForOldDays(context));
}// BaseFileAdapter()
@Override
public int getCount() {
/*
* The last item is used for information from the provider, we ignore
* it.
*/
int count = super.getCount();
return count > 0 ? count - 1 : 0;
}// getCount()
/**
* The "view holder"
*
* @author Hai Bison
*/
private static final class Bag {
ImageView mImageIcon;
ImageView mImageLockedSymbol;
TextView mTxtFileName;
TextView mTxtFileInfo;
CheckBox mCheckboxSelection;
}// Bag
private static class BagInfo {
boolean mChecked = false;
boolean mMarkedAsDeleted = false;
Uri mUri;
}// BagChildInfo
/**
* Map of child IDs to {@link BagChildInfo}.
*/
private final SparseArray<BagInfo> mSelectedChildrenMap = new SparseArray<BagInfo>();
@Override
public void bindView(View view, Context context, Cursor cursor) {
Bag bag = (Bag) view.getTag();
if (bag == null) {
bag = new Bag();
bag.mImageIcon = (ImageView) view
.findViewById(R.id.afc_imageview_icon);
bag.mImageLockedSymbol = (ImageView) view
.findViewById(R.id.afc_imageview_locked_symbol);
bag.mTxtFileName = (TextView) view
.findViewById(R.id.afc_textview_filename);
bag.mTxtFileInfo = (TextView) view
.findViewById(R.id.afc_textview_file_info);
bag.mCheckboxSelection = (CheckBox) view
.findViewById(R.id.afc_checkbox_selection);
view.setTag(bag);
}
final int id = cursor.getInt(cursor.getColumnIndex(BaseFile._ID));
final Uri uri = BaseFileProviderUtils.getUri(cursor);
final BagInfo bagInfo;
if (mSelectedChildrenMap.get(id) == null) {
bagInfo = new BagInfo();
bagInfo.mUri = uri;
mSelectedChildrenMap.put(id, bagInfo);
} else
bagInfo = mSelectedChildrenMap.get(id);
/*
* Update views.
*/
/*
* Use single line for grid view, multiline for list view
*/
bag.mTxtFileName.setSingleLine(view.getParent() instanceof GridView);
/*
* File icon.
*/
bag.mImageLockedSymbol.setVisibility(cursor.getInt(cursor
.getColumnIndex(BaseFile.COLUMN_CAN_READ)) > 0 ? View.GONE
: View.VISIBLE);
bag.mImageIcon.setImageResource(cursor.getInt(cursor
.getColumnIndex(BaseFile.COLUMN_ICON_ID)));
bag.mImageIcon.setOnTouchListener(mImageIconOnTouchListener);
bag.mImageIcon.setOnClickListener(BaseFileProviderUtils
.isDirectory(cursor) ? newImageIconOnClickListener(cursor
.getPosition()) : null);
/*
* Filename.
*/
bag.mTxtFileName.setText(BaseFileProviderUtils.getFileName(cursor));
Ui.strikeOutText(bag.mTxtFileName, bagInfo.mMarkedAsDeleted);
/*
* File info.
*/
String time = DateUtils.formatDate(context, cursor.getLong(cursor
.getColumnIndex(BaseFile.COLUMN_MODIFICATION_TIME)),
mFileTimeDisplay);
if (BaseFileProviderUtils.isFile(cursor))
bag.mTxtFileInfo.setText(String.format("%s, %s", Converter
.sizeToStr(cursor.getLong(cursor
.getColumnIndex(BaseFile.COLUMN_SIZE))), time));
else
bag.mTxtFileInfo.setText(time);
/*
* Check box.
*/
if (mMultiSelection) {
if (mFilterMode == BaseFile.FILTER_FILES_ONLY
&& BaseFileProviderUtils.isDirectory(cursor)) {
bag.mCheckboxSelection.setVisibility(View.GONE);
} else {
bag.mCheckboxSelection.setVisibility(View.VISIBLE);
bag.mCheckboxSelection.setOnCheckedChangeListener(null);
bag.mCheckboxSelection.setChecked(bagInfo.mChecked);
bag.mCheckboxSelection
.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(
CompoundButton buttonView, boolean isChecked) {
bagInfo.mChecked = isChecked;
}// onCheckedChanged()
});
bag.mCheckboxSelection
.setOnLongClickListener(mCheckboxSelectionOnLongClickListener);
}
} else
bag.mCheckboxSelection.setVisibility(View.GONE);
}// bindView()
@Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
synchronized (mSelectedChildrenMap) {
mSelectedChildrenMap.clear();
}
}// changeCursor()
/*
* UTILITIES.
*/
/**
* Sets the listener {@link OnBuildOptionsMenuListener}.
*
* @param listener
* the listener.
*/
public void setBuildOptionsMenuListener(OnBuildOptionsMenuListener listener) {
mOnBuildOptionsMenuListener = listener;
}// setBuildOptionsMenuListener()
/**
* Gets the listener {@link OnBuildOptionsMenuListener}.
*
* @return the listener.
*/
public OnBuildOptionsMenuListener getOnBuildOptionsMenuListener() {
return mOnBuildOptionsMenuListener;
}// getOnBuildOptionsMenuListener()
/**
* Gets the short name of this path.
*
* @return the path name, can be {@code null} if there is no data.
*/
public String getPathName() {
Cursor cursor = getCursor();
if (cursor == null || !cursor.moveToLast())
return null;
return BaseFileProviderUtils.getFileName(cursor);
}// getPathName()
/**
* Selects all items.
* <p/>
* <b>Note:</b> This will <i>not</i> notify data set for changes after done.
*
* @param fileType
* can be {@code -1} for all file types; or one of
* {@link BaseFile#FILE_TYPE_DIRECTORY},
* {@link BaseFile#FILE_TYPE_FILE}.
* @param selected
* {@code true} or {@code false}.
*/
private void asyncSelectAll(int fileType, boolean selected) {
int count = getCount();
for (int i = 0; i < count; i++) {
Cursor cursor = (Cursor) getItem(i);
int itemFileType = cursor.getInt(cursor
.getColumnIndex(BaseFile.COLUMN_TYPE));
if ((mFilterMode == BaseFile.FILTER_DIRECTORIES_ONLY && itemFileType == BaseFile.FILE_TYPE_FILE)
|| (mFilterMode == BaseFile.FILTER_FILES_ONLY && itemFileType == BaseFile.FILE_TYPE_DIRECTORY))
continue;
final int id = cursor.getInt(cursor.getColumnIndex(BaseFile._ID));
BagInfo b = mSelectedChildrenMap.get(id);
if (b == null) {
b = new BagInfo();
b.mUri = BaseFileProviderUtils.getUri(cursor);
mSelectedChildrenMap.put(id, b);
}
if (fileType >= 0 && itemFileType != fileType)
b.mChecked = false;
else if (b.mChecked != selected)
b.mChecked = selected;
}// for i
}// asyncSelectAll()
/**
* Selects all items.
* <p/>
* <b>Note:</b> This calls {@link #notifyDataSetChanged()} when done.
*
* @param selected
* {@code true} or {@code false}.
*/
public synchronized void selectAll(boolean selected) {
asyncSelectAll(-1, selected);
notifyDataSetChanged();
}// selectAll()
/**
* Inverts selection of all items.
* <p/>
* <b>Note:</b> This will <i>not</i> notify data set for changes after done.
*/
private void asyncInvertSelection() {
int count = getCount();
for (int i = 0; i < count; i++) {
Cursor cursor = (Cursor) getItem(i);
int fileType = cursor.getInt(cursor
.getColumnIndex(BaseFile.COLUMN_TYPE));
if ((mFilterMode == BaseFile.FILTER_DIRECTORIES_ONLY && fileType == BaseFile.FILE_TYPE_FILE)
|| (mFilterMode == BaseFile.FILTER_FILES_ONLY && fileType == BaseFile.FILE_TYPE_DIRECTORY))
continue;
final int id = cursor.getInt(cursor.getColumnIndex(BaseFile._ID));
BagInfo b = mSelectedChildrenMap.get(id);
if (b == null) {
b = new BagInfo();
b.mUri = BaseFileProviderUtils.getUri(cursor);
mSelectedChildrenMap.put(id, b);
}
b.mChecked = !b.mChecked;
}// for i
}// asyncInvertSelection()
/**
* Inverts selection of all items.
* <p/>
* <b>Note:</b> This calls {@link #notifyDataSetChanged()} after done.
*/
public synchronized void invertSelection() {
asyncInvertSelection();
notifyDataSetChanged();
}// invertSelection()
/**
* Checks if item with {@code id} is selected or not.
*
* @param id
* the database ID.
* @return {@code true} or {@code false}.
*/
public boolean isSelected(int id) {
synchronized (mSelectedChildrenMap) {
return mSelectedChildrenMap.get(id) != null ? mSelectedChildrenMap
.get(id).mChecked : false;
}
}// isSelected()
/**
* Gets selected items.
*
* @return list of URIs, can be empty.
*/
public ArrayList<Uri> getSelectedItems() {
ArrayList<Uri> res = new ArrayList<Uri>();
synchronized (mSelectedChildrenMap) {
for (int i = 0; i < mSelectedChildrenMap.size(); i++)
if (mSelectedChildrenMap.get(mSelectedChildrenMap.keyAt(i)).mChecked)
res.add(mSelectedChildrenMap.get(mSelectedChildrenMap
.keyAt(i)).mUri);
}
return res;
}// getSelectedItems()
/**
* Marks all selected items as deleted.
* <p/>
* <b>Note:</b> This calls {@link #notifyDataSetChanged()} after done.
*
* @param deleted
* {@code true} or {@code false}.
*/
public void markSelectedItemsAsDeleted(boolean deleted) {
synchronized (mSelectedChildrenMap) {
for (int i = 0; i < mSelectedChildrenMap.size(); i++)
if (mSelectedChildrenMap.get(mSelectedChildrenMap.keyAt(i)).mChecked)
mSelectedChildrenMap.get(mSelectedChildrenMap.keyAt(i)).mMarkedAsDeleted = deleted;
}
notifyDataSetChanged();
}// markSelectedItemsAsDeleted()
/**
* Marks specified item as deleted.
* <p/>
* <b>Note:</b> This calls {@link #notifyDataSetChanged()} after done.
*
* @param id
* the ID of the item.
* @param deleted
* {@code true} or {@code false}.
*/
public void markItemAsDeleted(int id, boolean deleted) {
synchronized (mSelectedChildrenMap) {
if (mSelectedChildrenMap.get(id) != null) {
mSelectedChildrenMap.get(id).mMarkedAsDeleted = deleted;
notifyDataSetChanged();
}
}
}// markItemAsDeleted()
/*
* LISTENERS
*/
/**
* If the user touches the list item, and the image icon <i>declared</i> a
* selector in XML, then that selector works. But we just want the selector
* to work only when the user touches the image, hence this listener.
*/
private final View.OnTouchListener mImageIconOnTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (Utils.doLog())
Log.d(CLASSNAME,
"mImageIconOnTouchListener.onTouch() >> ACTION = "
+ event.getAction());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
v.setBackgroundResource(R.drawable.afc_image_button_dark_pressed);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
v.setBackgroundResource(0);
break;
}
return false;
}// onTouch()
};// mImageIconOnTouchListener
/**
* Creates new listener to handle click event of image icon.
*
* @param cursorPosition
* the cursor position.
* @return the listener.
*/
private View.OnClickListener newImageIconOnClickListener(
final int cursorPosition) {
return new View.OnClickListener() {
@Override
public void onClick(View v) {
if (getOnBuildOptionsMenuListener() != null)
getOnBuildOptionsMenuListener().onBuildOptionsMenu(v,
(Cursor) getItem(cursorPosition));
}// onClick()
};
}// newImageIconOnClickListener()
private final View.OnLongClickListener mCheckboxSelectionOnLongClickListener = new View.OnLongClickListener() {
@Override
public boolean onLongClick(final View v) {
ContextMenuUtils.showContextMenu(v.getContext(), 0,
R.string.afc_title_advanced_selection,
mAdvancedSelectionOptions,
new ContextMenuUtils.OnMenuItemClickListener() {
@Override
public void onClick(final int resId) {
new LoadingDialog<Void, Void, Void>(v.getContext(),
R.string.afc_msg_loading, false) {
@Override
protected Void doInBackground(Void... params) {
if (resId == R.string.afc_cmd_advanced_selection_all)
asyncSelectAll(-1, true);
else if (resId == R.string.afc_cmd_advanced_selection_none)
asyncSelectAll(-1, false);
else if (resId == R.string.afc_cmd_advanced_selection_invert)
asyncInvertSelection();
else if (resId == R.string.afc_cmd_select_all_files)
asyncSelectAll(BaseFile.FILE_TYPE_FILE,
true);
else if (resId == R.string.afc_cmd_select_all_folders)
asyncSelectAll(
BaseFile.FILE_TYPE_DIRECTORY,
true);
return null;
}// doInBackground()
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
notifyDataSetChanged();
}// onPostExecute()
}.execute();
}// onClick()
});
return true;
}// onLongClick()
};// mCheckboxSelectionOnLongClickListener
}

View File

@ -0,0 +1,288 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser;
import group.pals.android.lib.ui.filechooser.prefs.DisplayPrefs;
import group.pals.android.lib.ui.filechooser.providers.basefile.BaseFileContract.BaseFile;
import group.pals.android.lib.ui.filechooser.providers.localfile.LocalFileContract;
import group.pals.android.lib.ui.filechooser.utils.Utils;
import group.pals.android.lib.ui.filechooser.utils.ui.Dlg;
import group.pals.android.lib.ui.filechooser.utils.ui.Ui;
import java.util.ArrayList;
import android.content.Context;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.GridView;
import android.widget.ListView;
/**
* Main activity for this library.
* <p/>
* <h1>Notes:</h1>
* <p/>
* <ol>
* <li>About keys {@link FileChooserActivity#EXTRA_ROOTPATH},
* {@link FileChooserActivity#EXTRA_SELECT_FILE} and preference
* {@link DisplayPrefs#isRememberLastLocation(Context)}, the priorities of them
* are:
* <ol>
* <li>{@link FileChooserActivity#EXTRA_SELECT_FILE}</li>
* <li>{@link FileChooserActivity#EXTRA_ROOTPATH}</li>
* <li>{@link DisplayPrefs#isRememberLastLocation(Context)}</li>
* </ol>
* </li>
* </ol>
*
* @author Hai Bison
*/
public class FileChooserActivity extends FragmentActivity {
/**
* The full name of this class. Generally used for debugging.
*/
private static final String CLASSNAME = FileChooserActivity.class.getName();
/**
* Types of view.
*
* @author Hai Bison
* @since v4.0 beta
*/
public static enum ViewType {
/**
* Use {@link ListView} to display file list.
*/
LIST,
/**
* Use {@link GridView} to display file list.
*/
GRID
}// ViewType
/*---------------------------------------------
* KEYS
*/
/**
* Sets value of this key to a theme which is one of {@code Afc_Theme_*}.
*
* @since v4.3 beta
*/
public static final String EXTRA_THEME = CLASSNAME + ".theme";
/**
* Key to hold the root path.
* <p/>
* If {@link LocalFileProvider} is used, then default is SD card, if SD card
* is not available, {@code "/"} will be used.
* <p/>
* <b>Note</b>: The value of this key is a file provider's {@link Uri}. For
* example with {@link LocalFileProvider}, you can use this command:
*
* <pre>
* <code>...
* intent.putExtra(FileChooserActivity.EXTRA_ROOTPATH,
* BaseFile.genContentIdUriBase(LocalFileContract.getAuthority())
* .buildUpon().appendPath("/sdcard").build())
* </code>
* </pre>
*/
public static final String EXTRA_ROOTPATH = CLASSNAME + ".rootpath";
/**
* Key to hold the authority of file provider.
* <p/>
* Default is {@link LocalFileContract#getAuthority(Context)}.
*/
public static final String EXTRA_FILE_PROVIDER_AUTHORITY = CLASSNAME
+ ".file_provider_authority";
// ---------------------------------------------------------
/**
* Key to hold filter mode, can be one of
* {@link BaseFile#FILTER_DIRECTORIES_ONLY},
* {@link BaseFile#FILTER_FILES_AND_DIRECTORIES},
* {@link BaseFile#FILTER_FILES_ONLY}.
* <p/>
* Default is {@link BaseFile#FILTER_FILES_ONLY}.
*/
public static final String EXTRA_FILTER_MODE = CLASSNAME + ".filter_mode";
// flags
// ---------------------------------------------------------
/**
* Key to hold max file count that's allowed to be listed, default =
* {@code 1000}.
*/
public static final String EXTRA_MAX_FILE_COUNT = CLASSNAME
+ ".max_file_count";
/**
* Key to hold multi-selection mode, default = {@code false}.
*/
public static final String EXTRA_MULTI_SELECTION = CLASSNAME
+ ".multi_selection";
/**
* Key to hold the positive regex to filter files (<b><i>not</i></b>
* directories), default is {@code null}.
*
* @since v5.1 beta
*/
public static final String EXTRA_POSITIVE_REGEX_FILTER = CLASSNAME
+ ".positive_regex_filter";
/**
* Key to hold the negative regex to filter files (<b><i>not</i></b>
* directories), default is {@code null}.
*
* @since v5.1 beta
*/
public static final String EXTRA_NEGATIVE_REGEX_FILTER = CLASSNAME
+ ".negative_regex_filter";
/**
* Key to hold display-hidden-files, default = {@code false}.
*/
public static final String EXTRA_DISPLAY_HIDDEN_FILES = CLASSNAME
+ ".display_hidden_files";
/**
* Sets this to {@code true} to enable double tapping to choose files/
* directories. In older versions, double tapping is default. However, since
* v4.7 beta, single tapping is default. So if you want to keep the old way,
* please set this key to {@code true}.
*
* @since v4.7 beta
*/
public static final String EXTRA_DOUBLE_TAP_TO_CHOOSE_FILES = CLASSNAME
+ ".double_tap_to_choose_files";
/**
* Sets the file you want to select when starting this activity. This is a
* file provider's {@link Uri}. For example with {@link LocalFileProvider},
* you can use this command:
* <p/>
*
* <pre>
* <code>...
* intent.putExtra(FileChooserActivity.EXTRA_SELECT_FILE,
* BaseFile.genContentIdUriBase(LocalFileContract.getAuthority())
* .buildUpon().appendPath("/sdcard").build())
* </code>
* </pre>
* <p/>
* <b>Notes:</b>
* <ul>
* <li>Currently this key is only used for single selection mode.</li>
* <li>If you use save dialog mode, this key will override key
* {@link #EXTRA_DEFAULT_FILENAME}.</li>
* </ul>
*
* @since v4.7 beta
*/
public static final String EXTRA_SELECT_FILE = CLASSNAME + ".select_file";
// ---------------------------------------------------------
/**
* Key to hold property save-dialog, default = {@code false}.
*/
public static final String EXTRA_SAVE_DIALOG = CLASSNAME + ".save_dialog";
/**
* Key to hold default filename, default = {@code null}.
*/
public static final String EXTRA_DEFAULT_FILENAME = CLASSNAME
+ ".default_filename";
/**
* Key to hold default file extension (<b>without</b> the period prefix),
* default = {@code null}.
* <p/>
* Note that this will be compared to the user's input value as
* case-insensitive. For example if you provide "csv" and the user types
* "CSV" then it is OK to use "CSV".
*/
public static final String EXTRA_DEFAULT_FILE_EXT = CLASSNAME
+ ".default_file_ext";
/**
* Key to hold results, which is an {@link ArrayList} of {@link Uri}. It can
* be one or multiple files.
*/
public static final String EXTRA_RESULTS = CLASSNAME + ".results";
public static final String EXTRA_RESULT_FILE_EXISTS = CLASSNAME + ".result_file_exists";
/*
* CONTROLS
*/
FragmentFiles mFragmentFiles;
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
/*
* EXTRA_THEME
*/
if (getIntent().hasExtra(EXTRA_THEME))
setTheme(getIntent().getIntExtra(EXTRA_THEME,
R.style.Afc_Theme_Dark));
super.onCreate(savedInstanceState);
setContentView(R.layout.afc_activity_filechooser);
Ui.adjustDialogSizeForLargeScreen(getWindow());
/*
* Make sure RESULT_CANCELED is default.
*/
setResult(RESULT_CANCELED);
mFragmentFiles = FragmentFiles.newInstance(getIntent());
getSupportFragmentManager().beginTransaction()
.add(R.id.afc_fragment_files, mFragmentFiles).commit();
}// onCreate()
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Ui.adjustDialogSizeForLargeScreen(getWindow());
}// onConfigurationChanged()
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (Utils.doLog())
Log.d(CLASSNAME, String.format("onKeyDown() >> %,d", keyCode));
if (keyCode == KeyEvent.KEYCODE_BACK) {
/*
* Use this hook instead of onBackPressed(), because onBackPressed()
* is not available in API 4.
*/
if (mFragmentFiles.isLoading()) {
if (Utils.doLog())
Log.d(CLASSNAME,
"onKeyDown() >> KEYCODE_BACK >> cancelling previous query...");
mFragmentFiles.cancelPreviousLoader();
Dlg.toast(this, R.string.afc_msg_cancelled, Dlg.LENGTH_SHORT);
return true;
}
}
return super.onKeyDown(keyCode, event);
}// onKeyDown()
}

View File

@ -0,0 +1,313 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.prefs;
import group.pals.android.lib.ui.filechooser.FileChooserActivity.ViewType;
import group.pals.android.lib.ui.filechooser.R;
import group.pals.android.lib.ui.filechooser.providers.basefile.BaseFileContract.BaseFile;
import android.content.Context;
/**
* Display preferences.
*
* @author Hai Bison
* @since v4.3 beta
*/
public class DisplayPrefs extends Prefs {
/**
* Delay time for waiting for other threads inside a thread... This is in
* milliseconds.
*/
public static final int DELAY_TIME_WAITING_THREADS = 10;
/**
* Delay time for waiting for very short animation, in milliseconds.
*/
public static final int DELAY_TIME_FOR_VERY_SHORT_ANIMATION = 199;
/**
* Delay time for waiting for short animation, in milliseconds.
*/
public static final int DELAY_TIME_FOR_SHORT_ANIMATION = 499;
/**
* Delay time for waiting for simple animation, in milliseconds.
*/
public static final int DELAY_TIME_FOR_SIMPLE_ANIMATION = 999;
/**
* Gets view type.
*
* @param c
* {@link Context}
* @return {@link ViewType}
*/
public static ViewType getViewType(Context c) {
return ViewType.LIST.ordinal() == p(c).getInt(
c.getString(R.string.afc_pkey_display_view_type),
c.getResources().getInteger(
R.integer.afc_pkey_display_view_type_def)) ? ViewType.LIST
: ViewType.GRID;
}
/**
* Sets view type.
*
* @param c
* {@link Context}
* @param v
* {@link ViewType}, if {@code null}, default value will be used.
*/
public static void setViewType(Context c, ViewType v) {
String key = c.getString(R.string.afc_pkey_display_view_type);
if (v == null)
p(c).edit()
.putInt(key,
c.getResources().getInteger(
R.integer.afc_pkey_display_view_type_def))
.commit();
else
p(c).edit().putInt(key, v.ordinal()).commit();
}
/**
* Gets sort type.
*
* @param c
* {@link Context}
* @return one of {@link BaseFile#SORT_BY_MODIFICATION_TIME},
* {@link BaseFile#SORT_BY_NAME}, {@link BaseFile#SORT_BY_SIZE}.
*/
public static int getSortType(Context c) {
return p(c).getInt(
c.getString(R.string.afc_pkey_display_sort_type),
c.getResources().getInteger(
R.integer.afc_pkey_display_sort_type_def));
}
/**
* Sets {@link SortType}
*
* @param c
* {@link Context}
* @param v
* one of {@link BaseFile#SORT_BY_MODIFICATION_TIME},
* {@link BaseFile#SORT_BY_NAME}, {@link BaseFile#SORT_BY_SIZE}.,
* if {@code null}, default value will be used.
*/
public static void setSortType(Context c, Integer v) {
String key = c.getString(R.string.afc_pkey_display_sort_type);
if (v == null)
p(c).edit()
.putInt(key,
c.getResources().getInteger(
R.integer.afc_pkey_display_sort_type_def))
.commit();
else
p(c).edit().putInt(key, v).commit();
}
/**
* Gets sort ascending.
*
* @param c
* {@link Context}
* @return {@code true} if sort is ascending, {@code false} otherwise.
*/
public static boolean isSortAscending(Context c) {
return p(c).getBoolean(
c.getString(R.string.afc_pkey_display_sort_ascending),
c.getResources().getBoolean(
R.bool.afc_pkey_display_sort_ascending_def));
}
/**
* Sets sort ascending.
*
* @param c
* {@link Context}
* @param v
* {@link Boolean}, if {@code null}, default value will be used.
*/
public static void setSortAscending(Context c, Boolean v) {
if (v == null)
v = c.getResources().getBoolean(
R.bool.afc_pkey_display_sort_ascending_def);
p(c).edit()
.putBoolean(
c.getString(R.string.afc_pkey_display_sort_ascending),
v).commit();
}
/**
* Checks setting of showing time for old days in this year. Default is
* {@code false}.
*
* @param c
* {@link Context}.
* @return {@code true} or {@code false}.
* @since v4.7 beta
*/
public static boolean isShowTimeForOldDaysThisYear(Context c) {
return p(c)
.getBoolean(
c.getString(R.string.afc_pkey_display_show_time_for_old_days_this_year),
c.getResources()
.getBoolean(
R.bool.afc_pkey_display_show_time_for_old_days_this_year_def));
}
/**
* Enables or disables showing time of old days in this year.
*
* @param c
* {@link Context}.
* @param v
* your preferred flag. If {@code null}, default will be used (
* {@code false}).
* @since v4.7 beta
*/
public static void setShowTimeForOldDaysThisYear(Context c, Boolean v) {
if (v == null)
v = c.getResources()
.getBoolean(
R.bool.afc_pkey_display_show_time_for_old_days_this_year_def);
p(c).edit()
.putBoolean(
c.getString(R.string.afc_pkey_display_show_time_for_old_days_this_year),
v).commit();
}
/**
* Checks setting of showing time for old days in last year and older.
* Default is {@code false}.
*
* @param c
* {@link Context}.
* @return {@code true} or {@code false}.
* @since v4.7 beta
*/
public static boolean isShowTimeForOldDays(Context c) {
return p(c).getBoolean(
c.getString(R.string.afc_pkey_display_show_time_for_old_days),
c.getResources().getBoolean(
R.bool.afc_pkey_display_show_time_for_old_days_def));
}
/**
* Enables or disables showing time of old days in last year and older.
*
* @param c
* {@link Context}.
* @param v
* your preferred flag. If {@code null}, default will be used (
* {@code false}).
* @since v4.7 beta
*/
public static void setShowTimeForOldDays(Context c, Boolean v) {
if (v == null)
v = c.getResources().getBoolean(
R.bool.afc_pkey_display_show_time_for_old_days_def);
p(c).edit()
.putBoolean(
c.getString(R.string.afc_pkey_display_show_time_for_old_days),
v).commit();
}
/**
* Checks if remembering last location is enabled or not.
*
* @param c
* {@link Context}.
* @return {@code true} if remembering last location is enabled.
* @since v4.7 beta
*/
public static boolean isRememberLastLocation(Context c) {
return false; //KP2A: don't allow to remember because of different protocols
}
/**
* Enables or disables remembering last location.
*
* @param c
* {@link Context}.
* @param v
* your preferred flag. If {@code null}, default will be used (
* {@code true}).
* @since v4.7 beta
*/
public static void setRememberLastLocation(Context c, Boolean v) {
if (v == null)
v = c.getResources().getBoolean(
R.bool.afc_pkey_display_remember_last_location_def);
p(c).edit()
.putBoolean(
c.getString(R.string.afc_pkey_display_remember_last_location),
v).commit();
}
/**
* Gets last location.
*
* @param c
* {@link Context}.
* @return the last location, or {@code null} if not available.
* @since v4.7 beta
*/
public static String getLastLocation(Context c) {
return p(c).getString(
c.getString(R.string.afc_pkey_display_last_location), null);
}
/**
* Sets last location.
*
* @param c
* {@link Context}.
* @param v
* the last location.
*/
public static void setLastLocation(Context c, String v) {
p(c).edit()
.putString(
c.getString(R.string.afc_pkey_display_last_location), v)
.commit();
}
/*
* HELPER CLASSES
*/
/**
* File time display options.
*
* @author Hai Bison
* @see DisplayPrefs#isShowTimeForOldDaysThisYear(Context)
* @see DisplayPrefs#isShowTimeForOldDays(Context)
* @since v4.9 beta
*/
public static class FileTimeDisplay {
public boolean showTimeForOldDaysThisYear;
public boolean showTimeForOldDays;
/**
* Creates new instance.
*
* @param showTimeForOldDaysThisYear
* @param showTimeForOldDays
*/
public FileTimeDisplay(boolean showTimeForOldDaysThisYear,
boolean showTimeForOldDays) {
this.showTimeForOldDaysThisYear = showTimeForOldDaysThisYear;
this.showTimeForOldDays = showTimeForOldDays;
}// FileTimeDisplay()
}// FileTimeDisplay
}

View File

@ -0,0 +1,83 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.prefs;
import group.pals.android.lib.ui.filechooser.utils.Sys;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceActivity;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
/**
* Convenient class for working with preferences.
*
* @author Hai Bison
* @since v4.3 beta
*/
public class Prefs {
/**
* This unique ID is used for storing preferences.
*
* @since v4.9 beta
*/
public static final String UID = "9795e88b-2ab4-4b81-a548-409091a1e0c6";
/**
* Generates global preference filename of this library.
*
* @return the global preference filename.
*/
public static final String genPreferenceFilename() {
return String.format("%s_%s", Sys.LIB_NAME, UID);
}
/**
* Generates global database filename.
*
* @param name
* the database filename.
* @return the global database filename.
*/
public static final String genDatabaseFilename(String name) {
return String.format("%s_%s_%s", Sys.LIB_NAME, UID, name);
}
/**
* Gets new {@link SharedPreferences}
*
* @param context
* the context.
* @return {@link SharedPreferences}
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static SharedPreferences p(Context context) {
// always use application context
return context.getApplicationContext().getSharedPreferences(
genPreferenceFilename(), Context.MODE_MULTI_PROCESS);
}
/**
* Setup {@code pm} to use global unique filename and global access mode.
* You must use this method if you let the user change preferences via UI
* (such as {@link PreferenceActivity}, {@link PreferenceFragment}...).
*
* @param pm
* {@link PreferenceManager}.
* @since v4.9 beta
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static void setupPreferenceManager(PreferenceManager pm) {
pm.setSharedPreferencesMode(Context.MODE_MULTI_PROCESS);
pm.setSharedPreferencesName(genPreferenceFilename());
}// setupPreferenceManager()
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.providers;
/**
* The base columns.
*
* @author Hai Bison
* @since v5.1 beta
*/
public interface BaseColumns extends android.provider.BaseColumns {
/**
* Column name for the creation timestamp.
* <p/>
* Type: {@code String} representing {@code long} from
* {@link java.util.Date#getTime()}. This is because SQLite doesn't handle
* Java's {@code long} well.
*/
public static final String COLUMN_CREATE_TIME = "create_time";
/**
* Column name for the modification timestamp.
* <p/>
* Type: {@code String} representing {@code long} from
* {@link java.util.Date#getTime()}. This is because SQLite doesn't handle
* Java's {@code long} well.
*/
public static final String COLUMN_MODIFICATION_TIME = "modification_time";
}

View File

@ -0,0 +1,653 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.providers;
import group.pals.android.lib.ui.filechooser.providers.basefile.BaseFileContract.BaseFile;
import group.pals.android.lib.ui.filechooser.providers.localfile.LocalFileProvider;
import group.pals.android.lib.ui.filechooser.utils.ui.Ui;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Bundle;
/**
* Utilities for base file provider.
*
* @author Hai Bison
* @since v5.1 beta
*/
public class BaseFileProviderUtils {
@SuppressWarnings("unused")
private static final String CLASSNAME = BaseFileProviderUtils.class
.getName();
/**
* Map of provider ID to its authority.
* <p/>
* <b>Note for developers:</b> If you provide your own provider, use
* {@link #registerProviderInfo(String, String)} to register it..
*/
private static final Map<String, Bundle> MAP_PROVIDER_INFO = new HashMap<String, Bundle>();
private static final String COLUMN_AUTHORITY = "authority";
/**
* Registers a file provider.
*
* @param id
* the provider ID. It should be a UUID.
* @param authority
* the autority.
*/
public static void registerProviderInfo(String id, String authority) {
Bundle bundle = new Bundle();
bundle.putString(COLUMN_AUTHORITY, authority);
MAP_PROVIDER_INFO.put(id, bundle);
}// registerProviderInfo()
/**
* Gets provider authority from its ID.
*
* @param providerId
* the provider ID.
* @return the provider authority, or {@code null} if not available.
*/
public static String getProviderAuthority(String providerId) {
return MAP_PROVIDER_INFO.get(providerId).getString(COLUMN_AUTHORITY);
}// getProviderAuthority()
/**
* Gets provider ID from its authority.
*
* @param authority
* the provider authority.
* @return the provider ID, or {@code null} if not available.
*/
public static String getProviderId(String authority) {
for (Entry<String, Bundle> entry : MAP_PROVIDER_INFO.entrySet())
if (entry.getValue().getString(COLUMN_AUTHORITY).equals(authority))
return entry.getKey();
return null;
}// getProviderId()
/**
* Gets provider name from its ID.
* <p/>
* <b>Note:</b> You should always use the method
* {@link #getProviderName(Context, String)} rather than this one whenever
* possible. Because this method does not guarantee the result.
*
* @param providerId
* the provider ID.
* @return the provider name, or {@code null} if not available.
*/
private static String getProviderName(String providerId) {
return MAP_PROVIDER_INFO.get(providerId).getString(
BaseFile.COLUMN_PROVIDER_NAME);
}// getProviderName()
/**
* Gets provider name from its ID.
*
* @param context
* {@link Context}.
* @param providerId
* the provider ID.
* @return the provider name, can be {@code null} if not provided.
*/
public static String getProviderName(Context context, String providerId) {
if (getProviderAuthority(providerId) == null)
return null;
String result = getProviderName(providerId);
if (result == null) {
Cursor cursor = context
.getContentResolver()
.query(BaseFile
.genContentUriApi(getProviderAuthority(providerId)),
null, null, null, null);
if (cursor == null)
return null;
try {
if (cursor.moveToFirst()) {
result = cursor.getString(cursor
.getColumnIndex(BaseFile.COLUMN_PROVIDER_NAME));
setProviderName(providerId, result);
} else
return null;
} finally {
cursor.close();
}
}
return result;
}// getProviderName()
/**
* Sets provider name.
*
* @param providerId
* the provider ID.
* @param providerName
* the provider name.
*/
private static void setProviderName(String providerId, String providerName) {
MAP_PROVIDER_INFO.get(providerId).putString(
BaseFile.COLUMN_PROVIDER_NAME, providerName);
}// setProviderName()
/**
* Gets the provider icon (badge) resource ID.
*
* @param context
* the context. The resource ID will be retrieved based on this
* context's theme (for example light or dark).
* @param providerId
* the provider ID.
* @return the resource ID of the icon (badge).
*/
public static int getProviderIconId(Context context, String providerId) {
int attr = MAP_PROVIDER_INFO.get(providerId).getInt(
BaseFile.COLUMN_PROVIDER_ICON_ATTR);
if (attr == 0) {
Cursor cursor = context
.getContentResolver()
.query(BaseFile
.genContentUriApi(getProviderAuthority(providerId)),
null, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
attr = cursor
.getInt(cursor
.getColumnIndex(BaseFile.COLUMN_PROVIDER_ICON_ATTR));
MAP_PROVIDER_INFO.get(providerId).putInt(
BaseFile.COLUMN_PROVIDER_ICON_ATTR, attr);
}
} finally {
cursor.close();
}
}
}
int res = Ui.resolveAttribute(context, attr);
if (res == 0)
res = attr;
return res;
}// getProviderIconId()
/**
* Default columns of a base file cursor.
* <p/>
* The column orders are:
* <p/>
* <ol>
* <li>{@link BaseFile#_ID}</li>
* <li>{@link BaseFile#COLUMN_URI}</li>
* <li>{@link BaseFile#COLUMN_REAL_URI}</li>
* <li>{@link BaseFile#COLUMN_NAME}</li>
* <li>{@link BaseFile#COLUMN_CAN_READ}</li>
* <li>{@link BaseFile#COLUMN_CAN_WRITE}</li>
* <li>{@link BaseFile#COLUMN_SIZE}</li>
* <li>{@link BaseFile#COLUMN_TYPE}</li>
* <li>{@link BaseFile#COLUMN_MODIFICATION_TIME}</li>
* <li>{@link BaseFile#COLUMN_ICON_ID}</li>
* </ol>
*/
public static final String[] BASE_FILE_CURSOR_COLUMNS = { BaseFile._ID,
BaseFile.COLUMN_URI, BaseFile.COLUMN_REAL_URI,
BaseFile.COLUMN_NAME, BaseFile.COLUMN_CAN_READ,
BaseFile.COLUMN_CAN_WRITE, BaseFile.COLUMN_SIZE,
BaseFile.COLUMN_TYPE, BaseFile.COLUMN_MODIFICATION_TIME,
BaseFile.COLUMN_ICON_ID };
/**
* Creates new cursor which holds default properties of a base file for
* client to access.
*
* @return the new empty cursor. The columns are
* {@link #BASE_FILE_CURSOR_COLUMNS}.
*/
public static MatrixCursor newBaseFileCursor() {
return new MatrixCursor(BASE_FILE_CURSOR_COLUMNS);
}// newBaseFileCursor()
/**
* Creates new cursor, closes it and returns it ^^
*
* @return the newly closed cursor.
*/
public static MatrixCursor newClosedCursor() {
MatrixCursor cursor = new MatrixCursor(new String[0]);
cursor.close();
return cursor;
}// newClosedCursor()
/**
* Checks if {@code uri} is a directory.
*
* @param context
* {@link Context}.
* @param uri
* the URI you want to check.
* @return {@code true} if {@code uri} is a directory, {@code false}
* otherwise.
*/
public static boolean isDirectory(Context context, Uri uri) {
Cursor cursor = context.getContentResolver().query(uri, null, null,
null, null);
if (cursor == null)
return false;
try {
if (cursor.moveToFirst())
return isDirectory(cursor);
return false;
} finally {
cursor.close();
}
}// isDirectory()
/**
* Checks if {@code cursor} is a directory.
*
* @param cursor
* the cursor points to a file.
* @return {@code true} if {@code cursor} is a directory, {@code false}
* otherwise.
*/
public static boolean isDirectory(Cursor cursor) {
return cursor.getInt(cursor.getColumnIndex(BaseFile.COLUMN_TYPE)) == BaseFile.FILE_TYPE_DIRECTORY;
}// isDirectory()
/**
* Checks if {@code uri} is a file.
*
* @param context
* {@link Context}.
* @param uri
* the URI you want to check.
* @return {@code true} if {@code uri} is a file, {@code false} otherwise.
*/
public static boolean isFile(Context context, Uri uri) {
Cursor cursor = context.getContentResolver().query(uri, null, null,
null, null);
if (cursor == null)
return false;
try {
if (cursor.moveToFirst())
return isFile(cursor);
return false;
} finally {
cursor.close();
}
}// isFile()
/**
* Checks if {@code cursor} is a file.
*
* @param cursor
* the cursor points to a file.
* @return {@code true} if {@code uri} is a file, {@code false} otherwise.
*/
public static boolean isFile(Cursor cursor) {
return cursor.getInt(cursor.getColumnIndex(BaseFile.COLUMN_TYPE)) == BaseFile.FILE_TYPE_FILE;
}// isFile()
/**
* Gets file name of {@code uri}.
*
* @param context
* {@link Context}.
* @param uri
* the URI you want to get.
* @return the file name if {@code uri} is a file, {@code null} otherwise.
*/
public static String getFileName(Context context, Uri uri) {
Cursor cursor = context.getContentResolver().query(uri, null, null,
null, null);
if (cursor == null)
return null;
try {
if (cursor.moveToFirst())
return getFileName(cursor);
return null;
} finally {
cursor.close();
}
}// getFileName()
/**
* Gets filename of {@code cursor}.
*
* @param cursor
* the cursor points to a file.
* @return the filename.
*/
public static String getFileName(Cursor cursor) {
return cursor.getString(cursor.getColumnIndex(BaseFile.COLUMN_NAME));
}// getFileName()
/**
* Gets the real URI of {@code uri}. This is independent of the content
* provider's URI ({@code uri}). For example with {@link LocalFileProvider},
* this method gets the URI which you can create new {@link File} object
* directly from it.
*
* @param context
* {@link Context}.
* @param uri
* the content provider URI which you want to get real URI from.
* @return the real URI of {@code uri}.
*/
public static Uri getRealUri(Context context, Uri uri) {
Cursor cursor = context.getContentResolver().query(uri, null, null,
null, null);
if (cursor == null)
return null;
try {
if (cursor.moveToFirst())
return getRealUri(cursor);
return null;
} finally {
cursor.close();
}
}// getRealUri()
/**
* Gets the real URI. This is independent of the content provider's URI
* which {@code cursor} points to. For example with
* {@link LocalFileProvider}, this method gets the URI which you can create
* new {@link File} object directly from it.
*
* @param cursor
* the cursor points to a file.
* @return the real URI.
*/
public static Uri getRealUri(Cursor cursor) {
return Uri.parse(cursor.getString(cursor
.getColumnIndex(BaseFile.COLUMN_REAL_URI)));
}// getRealUri()
/**
* Gets file type of the file pointed by {@code uri}.
*
* @param context
* {@link Context}.
* @param uri
* the URI you want to get.
* @return the file type of {@code uri}, can be one of
* {@link #FILE_TYPE_DIRECTORY}, {@link #FILE_TYPE_FILE},
* {@link #FILE_TYPE_UNKNOWN}, {@link #FILE_TYPE_NOT_EXISTED}.
*/
public static int getFileType(Context context, Uri uri) {
Cursor cursor = context.getContentResolver().query(uri, null, null,
null, null);
if (cursor == null)
return BaseFile.FILE_TYPE_NOT_EXISTED;
try {
if (cursor.moveToFirst())
return getFileType(cursor);
return BaseFile.FILE_TYPE_NOT_EXISTED;
} finally {
cursor.close();
}
}// getFileType()
/**
* Gets file type of the file pointed by {@code cursor}.
*
* @param cursor
* the cursor points to a file.
* @return the file type, can be one of {@link #FILE_TYPE_DIRECTORY},
* {@link #FILE_TYPE_FILE}, {@link #FILE_TYPE_UNKNOWN},
* {@link #FILE_TYPE_NOT_EXISTED}.
*/
public static int getFileType(Cursor cursor) {
return cursor.getInt(cursor.getColumnIndex(BaseFile.COLUMN_TYPE));
}// getFileType()
/**
* Gets URI of {@code cursor}.
*
* @param cursor
* the cursor points to a file.
* @return the URI.
*/
public static Uri getUri(Cursor cursor) {
return Uri.parse(cursor.getString(cursor
.getColumnIndex(BaseFile.COLUMN_URI)));
}// getFileName()
/**
* Checks if the file pointed by {@code uri} is existed or not.
*
* @param context
* {@link Context}.
* @param uri
* the URI you want to check.
* @return {@code true} or {@code false}.
*/
public static boolean fileExists(Context context, Uri uri) {
Cursor cursor = context.getContentResolver().query(uri, null, null,
null, null);
if (cursor == null)
return false;
try {
if (cursor.moveToFirst())
return cursor.getInt(cursor
.getColumnIndex(BaseFile.COLUMN_TYPE)) != BaseFile.FILE_TYPE_NOT_EXISTED;
return false;
} finally {
cursor.close();
}
}// fileExists()
/**
* Checks if the file pointed by {@code uri} is readable or not.
*
* @param context
* {@link Context}.
* @param uri
* the URI you want to check.
* @return {@code true} or {@code false}.
*/
public static boolean fileCanRead(Context context, Uri uri) {
Cursor cursor = context.getContentResolver().query(uri, null, null,
null, null);
if (cursor == null)
return false;
try {
if (cursor.moveToFirst())
return fileCanRead(cursor);
return false;
} finally {
cursor.close();
}
}// fileCanRead()
/**
* Checks if the file pointed be {@code cursor} is readable or not.
*
* @param cursor
* the cursor points to a file.
* @return {@code true} or {@code false}.
*/
public static boolean fileCanRead(Cursor cursor) {
if (cursor.getInt(cursor.getColumnIndex(BaseFile.COLUMN_CAN_READ)) != 0) {
switch (cursor.getInt(cursor.getColumnIndex(BaseFile.COLUMN_TYPE))) {
case BaseFile.FILE_TYPE_DIRECTORY:
case BaseFile.FILE_TYPE_FILE:
return true;
}
}
return false;
}// fileCanRead()
/**
* Checks if the file pointed by {@code uri} is writable or not.
*
* @param context
* {@link Context}.
* @param uri
* the URI you want to check.
* @return {@code true} or {@code false}.
*/
public static boolean fileCanWrite(Context context, Uri uri) {
Cursor cursor = context.getContentResolver().query(uri, null, null,
null, null);
if (cursor == null)
return false;
try {
if (cursor.moveToFirst())
return fileCanWrite(cursor);
return false;
} finally {
cursor.close();
}
}// fileCanWrite()
/**
* Checks if the file pointed by {@code cursor} is writable or not.
*
* @param cursor
* the cursor points to a file.
* @return {@code true} or {@code false}.
*/
public static boolean fileCanWrite(Cursor cursor) {
if (cursor.getInt(cursor.getColumnIndex(BaseFile.COLUMN_CAN_WRITE)) != 0) {
switch (cursor.getInt(cursor.getColumnIndex(BaseFile.COLUMN_TYPE))) {
case BaseFile.FILE_TYPE_DIRECTORY:
case BaseFile.FILE_TYPE_FILE:
return true;
}
}
return false;
}// fileCanWrite()
/**
* Gets default path of a provider.
*
* @param context
* {@link Context}.
* @param authority
* the provider's authority.
* @return the default path, can be {@code null}.
*/
public static Uri getDefaultPath(Context context, String authority) {
Cursor cursor = context.getContentResolver().query(
BaseFile.genContentUriApi(authority).buildUpon()
.appendPath(BaseFile.CMD_GET_DEFAULT_PATH).build(),
null, null, null, null);
if (cursor == null)
return null;
try {
if (cursor.moveToFirst())
return Uri.parse(cursor.getString(cursor
.getColumnIndex(BaseFile.COLUMN_URI)));
return null;
} finally {
cursor.close();
}
}// getDefaultPath()
/**
* Gets parent directory of {@code uri}.
*
* @param context
* {@link Context}.
* @param uri
* the URI of an existing file.
* @return the parent file if it exists, {@code null} otherwise.
*/
public static Uri getParentFile(Context context, Uri uri) {
Cursor cursor = context.getContentResolver().query(
BaseFile.genContentUriApi(uri.getAuthority())
.buildUpon()
.appendPath(BaseFile.CMD_GET_PARENT)
.appendQueryParameter(BaseFile.PARAM_SOURCE,
uri.getLastPathSegment()).build(), null, null,
null, null);
if (cursor == null)
return null;
try {
if (cursor.moveToFirst())
return Uri.parse(cursor.getString(cursor
.getColumnIndex(BaseFile.COLUMN_URI)));
return null;
} finally {
cursor.close();
}
}// getParentFile()
/**
* Checks if {@code uri1} is ancestor of {@code uri2}.
*
* @param context
* {@link Context}.
* @param uri1
* the first URI.
* @param uri2
* the second URI.
* @return {@code true} if {@code uri1} is ancestor of {@code uri2},
* {@code false} otherwise.
*/
public static boolean isAncestorOf(Context context, Uri uri1, Uri uri2) {
return context.getContentResolver().query(
BaseFile.genContentUriApi(uri1.getAuthority())
.buildUpon()
.appendPath(BaseFile.CMD_IS_ANCESTOR_OF)
.appendQueryParameter(BaseFile.PARAM_SOURCE,
uri1.getLastPathSegment())
.appendQueryParameter(BaseFile.PARAM_TARGET,
uri2.getLastPathSegment()).build(), null, null,
null, null) != null;
}// isAncestorOf()
/**
* Cancels a task with its ID.
*
* @param context
* the context.
* @param authority
* the file provider authority.
* @param taskId
* the task ID.
*/
public static void cancelTask(Context context, String authority, int taskId) {
context.getContentResolver().query(
BaseFile.genContentUriApi(authority)
.buildUpon()
.appendPath(BaseFile.CMD_CANCEL)
.appendQueryParameter(BaseFile.PARAM_TASK_ID,
Integer.toString(taskId)).build(), null, null,
null, null);
}// cancelTask()
}

View File

@ -0,0 +1,86 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.providers;
import android.database.DatabaseUtils;
/**
* Database utilities.
*
* @author Hai Bison
* @since v5.1 beta
*/
public class DbUtils {
public static final String DATE_FORMAT = "yyyy:MM:dd'T'kk:mm:ss";
/**
* SQLite component FTS3.
*
* @since v4.6 beta
*/
public static final String SQLITE_FTS3 = "FTS3";
/**
* SQLite component FTS4.
*
* @since v4.6 beta
*/
public static final String SQLITE_FTS4 = "FTS4";
/**
* Hidden column of FTS virtual table.
*/
public static final String SQLITE_FTS_COLUMN_ROW_ID = "rowid";
/**
* Joins all columns into one statement.
*
* @param cols
* array of columns.
* @return E.g: "col1,col2,col3"
*/
public static String joinColumns(String[] cols) {
if (cols == null)
return "";
StringBuffer sb = new StringBuffer();
for (String col : cols) {
sb.append(col).append(",");
}
return sb.toString().replaceAll(",$", "");
}// joinColumns()
/**
* Formats {@code n} to text to store to database. This method prefixes the
* output string with {@code "0"} to make sure the results will always have
* same length (for a {@link Long}). So it will work when comparing
* different values as text.
*
* @param n
* a long value.
* @return the formatted string.
*/
public static String formatNumber(long n) {
return String.format("%020d", n);
}// formatNumber()
/**
* Calls {@link DatabaseUtils#sqlEscapeString(String)}, then removes single
* quotes at the begin and the end of the returned string.
*
* @param value
* the string to escape. If {@code null}, empty string will
* return;
* @return the "raw" escaped-string.
*/
public static String rawSqlEscapeString(String value) {
return value == null ? "" : DatabaseUtils.sqlEscapeString(value)
.replaceFirst("(?msi)^'", "").replaceFirst("(?msi)'$", "");
}// rawSqlEscapeString()
}

View File

@ -0,0 +1,104 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.providers;
import android.content.ContentResolver;
import android.net.Uri;
/**
* Utilities for providers.
*
* @author Hai Bison
* @since v5.1 beta
*/
public class ProviderUtils {
/**
* The scheme part for default provider's URI.
*/
public static final String SCHEME = ContentResolver.SCHEME_CONTENT + "://";
/**
* Gets integer parameter.
*
* @param uri
* the original URI.
* @param key
* the key of query parameter.
* @param defaultValue
* will be returned if nothing found or parsing value failed.
* @return the integer value.
*/
public static int getIntQueryParam(Uri uri, String key, int defaultValue) {
try {
return Integer.parseInt(uri.getQueryParameter(key));
} catch (NumberFormatException e) {
return defaultValue;
}
}// getIntQueryParam()
/**
* Gets long parameter.
*
* @param uri
* the original URI.
* @param key
* the key of query parameter.
* @param defaultValue
* will be returned if nothing found or parsing value failed.
* @return the long value.
*/
public static long getLongQueryParam(Uri uri, String key, long defaultValue) {
try {
return Long.parseLong(uri.getQueryParameter(key));
} catch (NumberFormatException e) {
return defaultValue;
}
}// getLongQueryParam()
/**
* Gets boolean parameter.
*
* @param uri
* the original URI.
* @param key
* the key of query parameter.
* @return {@code false} if the parameter does not exist, or it is either
* {@code "false"} or {@code "0"}. {@code true} otherwise.
*/
public static boolean getBooleanQueryParam(Uri uri, String key) {
String param = uri.getQueryParameter(key);
if (param == null || Boolean.FALSE.toString().equalsIgnoreCase(param)
|| Integer.toString(0).equalsIgnoreCase(param))
return false;
return true;
}// getBooleanQueryParam()
/**
* Gets boolean parameter.
*
* @param uri
* the original URI.
* @param key
* the key of query parameter.
* @param defaultValue
* the default value if the parameter does not exist.
* @return {@code defaultValue} if the parameter does not exist, or it is
* either {@code "false"} or {@code "0"}. {@code true} otherwise.
*/
public static boolean getBooleanQueryParam(Uri uri, String key,
boolean defaultValue) {
String param = uri.getQueryParameter(key);
if (param == null)
return defaultValue;
if (param.matches("(?i)false|(0+)"))
return false;
return true;
}// getBooleanQueryParam()
}

View File

@ -0,0 +1,537 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.providers.basefile;
import group.pals.android.lib.ui.filechooser.providers.BaseColumns;
import group.pals.android.lib.ui.filechooser.providers.ProviderUtils;
import group.pals.android.lib.ui.filechooser.providers.localfile.FileObserverEx;
import group.pals.android.lib.ui.filechooser.providers.localfile.LocalFileProvider;
import java.io.File;
import android.content.ContentResolver;
import android.net.Uri;
/**
* Base file contract.
*
* @author Hai Bison
* @since v5.1 beta
*/
public class BaseFileContract {
/**
* This class cannot be instantiated.
*/
private BaseFileContract() {
}// BaseFileContract()
/**
* Base file.
*
* @author Hai Bison
* @since v5.1 beta
*/
public static final class BaseFile implements BaseColumns {
/**
* This class cannot be instantiated.
*/
private BaseFile() {
}// BaseFile()
/*
* FILE TYPE.
*/
/**
* Directory.
*/
public static final int FILE_TYPE_DIRECTORY = 0;
/**
* File.
*/
public static final int FILE_TYPE_FILE = 1;
/**
* UNKNOWN file type.
*/
public static final int FILE_TYPE_UNKNOWN = 2;
/**
* File is not existed.
*/
public static final int FILE_TYPE_NOT_EXISTED = 3;
/*
* FILTER MODE.
*/
/**
* Only files.
*/
public static final int FILTER_FILES_ONLY = 0;
/**
* Only directories.
*/
public static final int FILTER_DIRECTORIES_ONLY = 1;
/**
* Files and directories.
*/
public static final int FILTER_FILES_AND_DIRECTORIES = 2;
/*
* SORT MODE.
*/
/**
* Sort by name.
*/
public static final int SORT_BY_NAME = 0;
/**
* Sort by size.
*/
public static final int SORT_BY_SIZE = 1;
/**
* Sort by last modified.
*/
public static final int SORT_BY_MODIFICATION_TIME = 2;
/*
* PATHS
*/
/**
* <i>This is internal field.</i>
* <p/>
* The path to a single directory's contents. You query this path to get
* the contents of that directory.
*/
public static final String PATH_DIR = "dir";
/**
* <i>This is internal field.</i>
* <p/>
* The path to a single file. This can be a file or a directory.
*/
public static final String PATH_FILE = "file";
/**
* <i>This is internal field.</i>
* <p/>
* The path to query the provider's information such as name, ID...
*/
public static final String PATH_API = "api";
/*
* COMMANDS.
*/
/**
* Use this command to cancel a previous task you executed. You set the
* task ID with {@link #PARAM_TASK_ID}.
*
* @see #PARAM_TASK_ID
*/
public static final String CMD_CANCEL = "cancel";
/**
* Use this command along with two parameters: a source directory ID (
* {@link #PARAM_SOURCE}) and a target file/ directory ID (
* {@link #PARAM_TARGET}). It will return <i>a closed</i> cursor if the
* given source file is a directory and it is ancestor of the target
* file.
* <p/>
* If the given file is not a directory or is not ancestor of the file
* provided by this parameter, the result will be {@code null}.
* <p/>
* For example, with local file, this query returns {@code true}:
* <p/>
* {@code content://local-file-authority/api/is_ancestor_of?source="/mnt/sdcard"&target="/mnt/sdcard/Android/data/cache"}
* <p/>
* Note that no matter how many levels between the ancestor and the
* descendant are, it is still the ancestor. This is <b><i>not</i></b>
* the same concept as "parent", which will return {@code false} in
* above example.
*
* @see #PARAM_SOURCE
* @see #PARAM_TARGET
*/
public static final String CMD_IS_ANCESTOR_OF = "is_ancestor_of";
/**
* Use this command to get default path of a provider.
* <p/>
* Type: {@code String}
*/
public static final String CMD_GET_DEFAULT_PATH = "get_default_path";
/**
* Use this parameter to get parent file of a file. You provide the
* source file ID with {@link #PARAM_SOURCE}.
*
* @see #PARAM_SOURCE
*/
public static final String CMD_GET_PARENT = "get_parent";
/**
* Use this command when you don't need to work with the content
* provider anymore. Normally <i>Android handles ContentProvider startup
* and shutdown automatically</i>. But in case of
* {@link LocalFileProvider}, it uses {@link FileObserverEx} to watch
* for changes of files. The SDK doesn't clarify the ending events of a
* content provider. So the file-observer objects could continue to run
* even if your activity has stopped. Hence this command is useful to
* let the providers know when they can shutdown the background jobs.
*/
public static final String CMD_SHUTDOWN = "shutdown";
/*
* PARAMETERS.
*/
/**
* Use this parameter to provide the source file ID.
* <p/>
* Type: URI
*/
public static final String PARAM_SOURCE = "source";
/**
* Use this parameter to provide the target file ID.
* <p/>
* Type: URI
*/
public static final String PARAM_TARGET = "target";
/**
* Use this parameter to provide the name of new file/ directory you
* want to create.
* <p/>
* Type: {@code String}
*
* @see #PARAM_FILE_TYPE
*/
public static final String PARAM_NAME = "name";
/**
* Use this parameter to provide the type of new file that you want to
* create. It can be {@link #FILE_TYPE_DIRECTORY} or
* {@link #FILE_TYPE_FILE}. If not provided, default is
* {@link #FILE_TYPE_DIRECTORY}.
*
* @see #PARAM_NAME
*/
public static final String PARAM_FILE_TYPE = "file_type";
/**
* Use this parameter to set an ID to any task.
* <p/>
* Default: {@code 0} with all methods.
* <p/>
* Type: {@code Integer}
*/
public static final String PARAM_TASK_ID = "task_id";
/**
* Use this parameter for operators which can work recursively, such as
* deleting a directory... The value can be {@code "true"} or
* {@code "1"} for {@code true}, {@code "false"} or {@code "0"} for
* {@code false}.
* <p/>
* Default:
* <p/>
* <ul>
* <li>{@code "true"} with {@code delete()}.</li>
* </ul>
* <p/>
* Type: {@code Boolean}
*/
public static final String PARAM_RECURSIVE = "recursive";
/**
* Use this parameter to show hidden files. The value can be
* {@code "true"} or {@code "1"} for {@code true}, {@code "false"} or
* {@code "0"} for {@code false}.
* <p/>
* Default: {@code "false"} with {@code query()}.
* <p/>
* Type: {@code Boolean}
*/
public static final String PARAM_SHOW_HIDDEN_FILES = "show_hidden_files";
/**
* Use this parameter to filter file type. Can be one of
* {@link #FILTER_FILES_ONLY}, {@link #FILTER_DIRECTORIES_ONLY},
* {@link #FILTER_FILES_AND_DIRECTORIES}.
* <p/>
* Default: {@link #FILTER_FILES_AND_DIRECTORIES} with {@code query()}.
* <p/>
* Type: {@code Integer}
*/
public static final String PARAM_FILTER_MODE = "filter_mode";
/**
* Use this parameter to sort files. Can be one of
* {@link #SORT_BY_MODIFICATION_TIME}, {@link #SORT_BY_NAME},
* {@link #SORT_BY_SIZE}.
* <p/>
* Default: {@link #SORT_BY_NAME} with {@code query()}.
* <p/>
* Type: {@code Integer}
*/
public static final String PARAM_SORT_BY = "sort_by";
/**
* Use this parameter for sort order. Can be {@code "true"} or
* {@code "1"} for {@code true}, {@code "false"} or {@code "0"} for
* {@code false}.
* <p/>
* Default: {@code "true"} with {@code query()}.
* <p/>
* Type: {@code Boolean}
*/
public static final String PARAM_SORT_ASCENDING = "sort_ascending";
/**
* Use this parameter to limit results.
* <p/>
* Default: {@code 1000} with {@code query()}.
* <p/>
* Type: {@code Integer}
*/
public static final String PARAM_LIMIT = "limit";
/**
* This parameter is returned from the provider. It's only used for
* {@code query()} while querying directory contents. Can be
* {@code "true"} or {@code "1"} for {@code true}, {@code "false"} or
* {@code "0"} for {@code false}.
* <p/>
* Type: {@code Boolean}
*/
public static final String PARAM_HAS_MORE_FILES = "has_more_files";
/**
* Use this parameter to append a file name to a full path of directory
* to obtains its full pathname.
* <p/>
* This parameter can be use together with {@link #PARAM_APPEND_PATH},
* the priority is lesser than that parameter.
* <p/>
* <ul>
* <li>Scope:
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* and related.</li>
* </ul>
* <p/>
* Type: {@code String}
*/
public static final String PARAM_APPEND_NAME = "append_name";
/**
* Use this parameter to append a partial path to a full path of
* directory to obtains its full pathname. The value is a URI, every
* path segment of the URI is a partial name. You can build the URI with
* scheme {@link ContentResolver#SCHEME_FILE}, appending your paths with
* {@link Uri.Builder#appendPath(String)}.
* <p/>
* This parameter can be use together with {@link #PARAM_APPEND_NAME},
* the priority is higher than that parameter.
* <p/>
* <ul>
* <li>Scope:
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* and related.</li>
* </ul>
* <p/>
* Type: {@code String}
*
* @see #PARAM_APPEND_NAME
*/
public static final String PARAM_APPEND_PATH = "append_path";
/**
* Use this parameter to set a positive regex to filter filename (with
* {@code query()}). If the regex can't be compiled due to syntax error,
* then it will be ignored.
* <p/>
* Type: {@code String}
*/
public static final String PARAM_POSITIVE_REGEX_FILTER = "positive_regex_filter";
/**
* Use this parameter to set a negative regex to filter filename (with
* {@code query()}). If the regex can't be compiled due to syntax error,
* then it will be ignored.
* <p/>
* Type: {@code String}
*/
public static final String PARAM_NEGATIVE_REGEX_FILTER = "negative_regex_filter";
/**
* Use this parameter to tell the provider to validate files or not.
* <p/>
* Type: {@code String} - can be {@code "true"} or {@code "1"} for
* {@code true}, {@code "false"} or {@code "0"} for {@code false}.
* <p/>
* Scope:
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* and related.
* <p/>
* Default: {@code true}
*
* @see #CMD_IS_ANCESTOR_OF
*/
public static final String PARAM_VALIDATE = "validate";
/*
* URI builders.
*/
/**
* Generates content URI API for a provider.
*
* @param authority
* the authority of file provider.
* @return The API URI for a provider. Default will return provider name
* and ID.
*/
public static Uri genContentUriApi(String authority) {
return Uri.parse(ProviderUtils.SCHEME + authority + "/" + PATH_API);
}// genContentUriBase()
/**
* Generates content URI base for a single directory's contents. That
* means this URI is used to get the content of the given directory,
* <b><i>not</b></i> the attributes of its. To get the attributes of a
* directory (or a file), use {@link #genContentIdUriBase(String)}.
*
* @param authority
* the authority of file provider.
* @return The base URI for a single directory. You append it with the
* URI to full path of the directory.
*/
public static Uri genContentUriBase(String authority) {
return Uri.parse(ProviderUtils.SCHEME + authority + "/" + PATH_DIR
+ "/");
}// genContentUriBase()
/**
* Generates content URI base for a single file.
*
* @param authority
* the authority of file provider.
* @return The base URI for a single file. You append it with the URI to
* full path of a single file.
*/
public static Uri genContentIdUriBase(String authority) {
return Uri.parse(ProviderUtils.SCHEME + authority + "/" + PATH_FILE
+ "/");
}// genContentIdUriBase()
/*
* MIME type definitions.
*/
/**
* The MIME type providing a directory of files.
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.android-filechooser.basefile";
/**
* The MIME type of a single file.
*/
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.android-filechooser.basefile";
/*
* Column definitions
*/
/**
* The URI of this file.
* <p/>
* Type: {@code String}
*/
public static final String COLUMN_URI = "uri";
/**
* The real URI of this file. This URI is independent of the content
* provider's URI. For example with {@link LocalFileProvider}, this
* column contains the URI which you can create new {@link File} object
* directly from it.
* <p/>
* Type: {@code String}
*/
public static final String COLUMN_REAL_URI = "real_uri";
/**
* The name of this file.
* <p/>
* Type: {@code String}
*/
public static final String COLUMN_NAME = "name";
/**
* Size of this file.
* <p/>
* Type: {@code Long}
*/
public static final String COLUMN_SIZE = "size";
/**
* Holds the readable attribute of this file, {@code 0 == false} and
* {@code 1 == true}.
* <p/>
* Type: {@code Integer}
*/
public static final String COLUMN_CAN_READ = "can_read";
/**
* Holds the writable attribute of this file, {@code 0 == false} and
* {@code 1 == true}.
* <p/>
* Type: {@code Integer}
*/
public static final String COLUMN_CAN_WRITE = "can_write";
/**
* The type of this file. Can be one of {@link #FILE_TYPE_DIRECTORY},
* {@link #FILE_TYPE_FILE}, {@link #FILE_TYPE_UNKNOWN},
* {@link #FILE_TYPE_NOT_EXISTED}.
* <p/>
* Type: {@code Integer}
*/
public static final String COLUMN_TYPE = "type";
/**
* The resource ID of the file icon.
* <p/>
* Type: {@code Integer}
*/
public static final String COLUMN_ICON_ID = "icon_id";
/**
* The name of this provider.
* <p/>
* Type: {@code String}
*/
public static final String COLUMN_PROVIDER_NAME = "provider_name";
/**
* The ID of this provider.
* <p/>
* Type: {@code String}
*/
public static final String COLUMN_PROVIDER_ID = "provider_id";
/**
* The resource ID ({@code R.attr}) of the badge (icon) of the provider.
* <p/>
* Type: {@code Integer}
*/
public static final String COLUMN_PROVIDER_ICON_ATTR = "provider_icon_attr";
}// BaseFile
}

View File

@ -0,0 +1,127 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.providers.basefile;
import group.pals.android.lib.ui.filechooser.providers.basefile.BaseFileContract.BaseFile;
import java.text.Collator;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.util.SparseBooleanArray;
/**
* Base provider for files.
*
* @author Hai Bison
* @since v5.1 beta
*/
public abstract class BaseFileProvider extends ContentProvider {
/*
* Constants used by the Uri matcher to choose an action based on the
* pattern of the incoming URI.
*/
/**
* The incoming URI matches the directory's contents URI pattern.
*/
protected static final int URI_DIRECTORY = 1;
/**
* The incoming URI matches the single file URI pattern.
*/
protected static final int URI_FILE = 2;
/**
* The incoming URI matches the identification URI pattern.
*/
protected static final int URI_API = 3;
/**
* The incoming URI matches the API command URI pattern.
*/
protected static final int URI_API_COMMAND = 4;
/**
* A {@link UriMatcher} instance.
*/
protected static final UriMatcher URI_MATCHER = new UriMatcher(
UriMatcher.NO_MATCH);
/**
* Map of task IDs to their interruption signals.
*/
protected final SparseBooleanArray mMapInterruption = new SparseBooleanArray();
/**
* This collator is used to compare file names.
*/
protected final Collator mCollator = Collator.getInstance();
@Override
public boolean onCreate() {
return true;
}// onCreate()
@Override
public String getType(Uri uri) {
/*
* Chooses the MIME type based on the incoming URI pattern.
*/
switch (URI_MATCHER.match(uri)) {
case URI_API:
case URI_API_COMMAND:
case URI_DIRECTORY:
return BaseFile.CONTENT_TYPE;
case URI_FILE:
return BaseFile.CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("UNKNOWN URI " + uri);
}
}// getType()
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
/*
* Do nothing.
*/
return 0;
}// delete()
@Override
public Uri insert(Uri uri, ContentValues values) {
/*
* Do nothing.
*/
return null;
}// insert()
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
/*
* Do nothing.
*/
return null;
}// query()
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
/*
* Do nothing.
*/
return 0;
}// update()
}

View File

@ -0,0 +1,127 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.providers.history;
import group.pals.android.lib.ui.filechooser.providers.BaseColumns;
import group.pals.android.lib.ui.filechooser.providers.ProviderUtils;
import group.pals.android.lib.ui.filechooser.providers.basefile.BaseFileContract.BaseFile;
import android.content.Context;
import android.net.Uri;
/**
* History contract.
*
* @author Hai Bison
* @since v5.1 beta
*/
public final class HistoryContract implements BaseColumns {
/**
* The raw authority.
*/
private static final String AUTHORITY = "android-filechooser.history";
/**
* Gets the authority of this provider.
*
* @param context
* the context.
* @return the authority.
*/
public static final String getAuthority(Context context) {
return context.getPackageName() + "." + AUTHORITY;
}// getAuthority()
// This class cannot be instantiated
private HistoryContract() {
}
/**
* The table name offered by this provider.
*/
public static final String TABLE_NAME = "history";
/*
* URI definitions.
*/
/**
* Path parts for the URIs.
*/
/**
* Path part for the History URI.
*/
public static final String PATH_HISTORY = "history";
/**
* The content:// style URL for this table.
*/
public static final Uri genContentUri(Context context) {
return Uri.parse(ProviderUtils.SCHEME + getAuthority(context) + "/"
+ PATH_HISTORY);
}// genContentUri()
/**
* The content URI base for a single history item. Callers must append a
* numeric history ID to this Uri to retrieve a history item.
*/
public static final Uri genContentIdUriBase(Context context) {
return Uri.parse(ProviderUtils.SCHEME + getAuthority(context) + "/"
+ PATH_HISTORY + "/");
}
/*
* MIME type definitions.
*/
/**
* The MIME type of {@link #_ContentUri} providing a directory of history
* items.
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.android-filechooser.history";
/**
* The MIME type of a {@link #_ContentUri} sub-directory of a single history
* item.
*/
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.android-filechooser.history";
/**
* The default sort order for this table.
*/
public static final String DEFAULT_SORT_ORDER = COLUMN_MODIFICATION_TIME
+ " DESC";
/*
* Column definitions.
*/
/**
* Column name for the ID of the provider.
* <p/>
* Type: {@code String}
*/
public static final String COLUMN_PROVIDER_ID = "provider_id";
/**
* Column name for the type of history. The value can be one of
* {@link BaseFile#FILE_TYPE_DIRECTORY}, {@link BaseFile#FILE_TYPE_FILE}.
* <p/>
* Type: {@code Integer}
*/
public static final String COLUMN_FILE_TYPE = "file_type";
/**
* Column name for the URI of history.
* <p/>
* Type: {@code URI}
*/
public static final String COLUMN_URI = "uri";
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.providers.history;
import group.pals.android.lib.ui.filechooser.prefs.Prefs;
import group.pals.android.lib.ui.filechooser.providers.DbUtils;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Build;
/**
* SQLite helper for history database.
*
* @author Hai Bison
* @since v5.1 beta
*/
public class HistoryHelper extends SQLiteOpenHelper {
private static final String DB_FILENAME = "History.sqlite";
private static final int DB_VERSION = 1;
/**
* @since v5.1 beta
*/
private static final String PATTERN_DB_CREATOR_V3 = String
.format("CREATE VIRTUAL TABLE " + HistoryContract.TABLE_NAME
+ " USING %%s(" + HistoryContract.COLUMN_CREATE_TIME + ","
+ HistoryContract.COLUMN_MODIFICATION_TIME + ","
+ HistoryContract.COLUMN_PROVIDER_ID + ","
+ HistoryContract.COLUMN_FILE_TYPE + ","
+ HistoryContract.COLUMN_URI + ",tokenize=porter);");
public HistoryHelper(Context context) {
// always use application context
super(context.getApplicationContext(), Prefs
.genDatabaseFilename(DB_FILENAME), null, DB_VERSION);
}// HistoryHelper()
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(String
.format(PATTERN_DB_CREATOR_V3,
Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ? DbUtils.SQLITE_FTS3
: DbUtils.SQLITE_FTS4));
}// onCreate()
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO
}// onUpgrade()
}

View File

@ -0,0 +1,427 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.providers.history;
import group.pals.android.lib.ui.filechooser.BuildConfig;
import group.pals.android.lib.ui.filechooser.providers.BaseFileProviderUtils;
import group.pals.android.lib.ui.filechooser.providers.DbUtils;
import group.pals.android.lib.ui.filechooser.providers.basefile.BaseFileContract.BaseFile;
import group.pals.android.lib.ui.filechooser.utils.Utils;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
/**
* History provider.
*
* @author Hai Bison
* @since v5.1 beta
*/
public class HistoryProvider extends ContentProvider {
private static final String CLASSNAME = HistoryProvider.class.getName();
/*
* Constants used by the Uri matcher to choose an action based on the
* pattern of the incoming URI.
*/
/**
* The incoming URI matches the history URI pattern.
*/
private static final int URI_HISTORY = 1;
/**
* The incoming URI matches the history ID URI pattern.
*/
private static final int URI_HISTORY_ID = 2;
/**
* A {@link UriMatcher} instance.
*/
private static final UriMatcher URI_MATCHER = new UriMatcher(
UriMatcher.NO_MATCH);
private static final Map<String, String> MAP_COLUMNS = new HashMap<String, String>();
static {
MAP_COLUMNS
.put(DbUtils.SQLITE_FTS_COLUMN_ROW_ID,
DbUtils.SQLITE_FTS_COLUMN_ROW_ID + " AS "
+ HistoryContract._ID);
MAP_COLUMNS.put(HistoryContract.COLUMN_PROVIDER_ID,
HistoryContract.COLUMN_PROVIDER_ID);
MAP_COLUMNS.put(HistoryContract.COLUMN_FILE_TYPE,
HistoryContract.COLUMN_FILE_TYPE);
MAP_COLUMNS.put(HistoryContract.COLUMN_URI, HistoryContract.COLUMN_URI);
MAP_COLUMNS.put(HistoryContract.COLUMN_CREATE_TIME,
HistoryContract.COLUMN_CREATE_TIME);
MAP_COLUMNS.put(HistoryContract.COLUMN_MODIFICATION_TIME,
HistoryContract.COLUMN_MODIFICATION_TIME);
}// static
private HistoryHelper mHistoryHelper;
@Override
public boolean onCreate() {
mHistoryHelper = new HistoryHelper(getContext());
URI_MATCHER.addURI(HistoryContract.getAuthority(getContext()),
HistoryContract.PATH_HISTORY, URI_HISTORY);
URI_MATCHER.addURI(HistoryContract.getAuthority(getContext()),
HistoryContract.PATH_HISTORY + "/#", URI_HISTORY_ID);
return true;
}// onCreate()
@Override
public String getType(Uri uri) {
/*
* Chooses the MIME type based on the incoming URI pattern.
*/
switch (URI_MATCHER.match(uri)) {
case URI_HISTORY:
return HistoryContract.CONTENT_TYPE;
case URI_HISTORY_ID:
return HistoryContract.CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("UNKNOWN URI " + uri);
}
}// getType()
@Override
public synchronized int delete(Uri uri, String selection,
String[] selectionArgs) {
// Opens the database object in "write" mode.
SQLiteDatabase db = mHistoryHelper.getWritableDatabase();
String finalWhere;
int count;
// Does the delete based on the incoming URI pattern.
switch (URI_MATCHER.match(uri)) {
/*
* If the incoming pattern matches the general pattern for history
* items, does a delete based on the incoming "where" columns and
* arguments.
*/
case URI_HISTORY:
count = db.delete(HistoryContract.TABLE_NAME, selection,
selectionArgs);
break;// URI_HISTORY
/*
* If the incoming URI matches a single note ID, does the delete based
* on the incoming data, but modifies the where clause to restrict it to
* the particular history item ID.
*/
case URI_HISTORY_ID:
/*
* Starts a final WHERE clause by restricting it to the desired
* history item ID.
*/
finalWhere = DbUtils.SQLITE_FTS_COLUMN_ROW_ID + " = "
+ uri.getLastPathSegment();
/*
* If there were additional selection criteria, append them to the
* final WHERE clause
*/
if (selection != null)
finalWhere = finalWhere + " AND " + selection;
// Performs the delete.
count = db.delete(HistoryContract.TABLE_NAME, finalWhere,
selectionArgs);
break;// URI_HISTORY_ID
// If the incoming pattern is invalid, throws an exception.
default:
throw new IllegalArgumentException("UNKNOWN URI " + uri);
}
/*
* Gets a handle to the content resolver object for the current context,
* and notifies it that the incoming URI changed. The object passes this
* along to the resolver framework, and observers that have registered
* themselves for the provider are notified.
*/
getContext().getContentResolver().notifyChange(uri, null);
// Returns the number of rows deleted.
return count;
}// delete()
@Override
public synchronized Uri insert(Uri uri, ContentValues values) {
/*
* Validates the incoming URI. Only the full provider URI is allowed for
* inserts.
*/
if (URI_MATCHER.match(uri) != URI_HISTORY)
throw new IllegalArgumentException("UNKNOWN URI " + uri);
// Gets the current time in milliseconds
long now = new Date().getTime();
/*
* If the values map doesn't contain the creation date/ modification
* date, sets the value to the current time.
*/
for (String col : new String[] { HistoryContract.COLUMN_CREATE_TIME,
HistoryContract.COLUMN_MODIFICATION_TIME })
if (!values.containsKey(col))
values.put(col, DbUtils.formatNumber(now));
// Opens the database object in "write" mode.
SQLiteDatabase db = mHistoryHelper.getWritableDatabase();
// Performs the insert and returns the ID of the new note.
long rowId = db.insert(HistoryContract.TABLE_NAME, null, values);
// If the insert succeeded, the row ID exists.
if (rowId > 0) {
/*
* Creates a URI with the note ID pattern and the new row ID
* appended to it.
*/
Uri noteUri = ContentUris.withAppendedId(
HistoryContract.genContentIdUriBase(getContext()), rowId);
/*
* Notifies observers registered against this provider that the data
* changed.
*/
getContext().getContentResolver().notifyChange(noteUri, null);
return noteUri;
}
/*
* If the insert didn't succeed, then the rowID is <= 0. Throws an
* exception.
*/
throw new SQLException("Failed to insert row into " + uri);
}// insert()
@Override
public synchronized Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder) {
if (Utils.doLog())
Log.d(CLASSNAME, String.format(
"query() >> uri = %s, selection = %s, sortOrder = %s", uri,
selection, sortOrder));
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(HistoryContract.TABLE_NAME);
qb.setProjectionMap(MAP_COLUMNS);
SQLiteDatabase db = null;
Cursor cursor = null;
/*
* Choose the projection and adjust the "where" clause based on URI
* pattern-matching.
*/
switch (URI_MATCHER.match(uri)) {
case URI_HISTORY: {
if (Arrays.equals(projection,
new String[] { HistoryContract._COUNT })) {
db = mHistoryHelper.getReadableDatabase();
cursor = db.rawQuery(
String.format(
"SELECT COUNT(*) AS %s FROM %s %s",
HistoryContract._COUNT,
HistoryContract.TABLE_NAME,
selection != null ? String.format("WHERE %s",
selection) : "").trim(), null);
}
break;
}// URI_HISTORY
/*
* If the incoming URI is for a single history item identified by its
* ID, chooses the history item ID projection, and appends
* "_ID = <history-item-ID>" to the where clause, so that it selects
* that single history item.
*/
case URI_HISTORY_ID: {
qb.appendWhere(DbUtils.SQLITE_FTS_COLUMN_ROW_ID + " = "
+ uri.getLastPathSegment());
break;
}// URI_HISTORY_ID
default:
throw new IllegalArgumentException("UNKNOWN URI " + uri);
}
if (TextUtils.isEmpty(sortOrder))
sortOrder = HistoryContract.DEFAULT_SORT_ORDER;
/*
* Opens the database object in "read" mode, since no writes need to be
* done.
*/
if (Utils.doLog())
Log.d(CLASSNAME,
String.format("Going to SQLiteQueryBuilder >> db = %s", db));
if (db == null) {
db = mHistoryHelper.getReadableDatabase();
/*
* Performs the query. If no problems occur trying to read the
* database, then a Cursor object is returned; otherwise, the cursor
* variable contains null. If no records were selected, then the
* Cursor object is empty, and Cursor.getCount() returns 0.
*/
cursor = qb.query(db, projection, selection, selectionArgs, null,
null, sortOrder);
}
cursor = appendNameAndRealUri(cursor);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}// query()
@Override
public synchronized int update(Uri uri, ContentValues values,
String selection, String[] selectionArgs) {
// Opens the database object in "write" mode.
SQLiteDatabase db = mHistoryHelper.getWritableDatabase();
int count;
String finalWhere;
// Does the update based on the incoming URI pattern
switch (URI_MATCHER.match(uri)) {
/*
* If the incoming URI matches the general history items pattern, does
* the update based on the incoming data.
*/
case URI_HISTORY:
// Does the update and returns the number of rows updated.
count = db.update(HistoryContract.TABLE_NAME, values, selection,
selectionArgs);
break;
/*
* If the incoming URI matches a single history item ID, does the update
* based on the incoming data, but modifies the where clause to restrict
* it to the particular history item ID.
*/
case URI_HISTORY_ID:
/*
* Starts creating the final WHERE clause by restricting it to the
* incoming item ID.
*/
finalWhere = DbUtils.SQLITE_FTS_COLUMN_ROW_ID + " = "
+ uri.getLastPathSegment();
/*
* If there were additional selection criteria, append them to the
* final WHERE clause
*/
if (selection != null)
finalWhere = finalWhere + " AND " + selection;
// Does the update and returns the number of rows updated.
count = db.update(HistoryContract.TABLE_NAME, values, finalWhere,
selectionArgs);
break;
// If the incoming pattern is invalid, throws an exception.
default:
throw new IllegalArgumentException("UNKNOWN URI " + uri);
}
/*
* Gets a handle to the content resolver object for the current context,
* and notifies it that the incoming URI changed. The object passes this
* along to the resolver framework, and observers that have registered
* themselves for the provider are notified.
*/
getContext().getContentResolver().notifyChange(uri, null);
// Returns the number of rows updated.
return count;
}// update()
private static final String[] ADDITIONAL_COLUMNS = { BaseFile.COLUMN_NAME,
BaseFile.COLUMN_REAL_URI };
/**
* Appends file name and real URI into {@code cursor}.
*
* @param cursor
* the original cursor. It will be closed when done.
* @return the new cursor.
*/
private Cursor appendNameAndRealUri(Cursor cursor) {
if (cursor == null || cursor.getCount() == 0)
return cursor;
final int colUri = cursor.getColumnIndex(HistoryContract.COLUMN_URI);
if (colUri < 0)
return cursor;
String[] columns = new String[cursor.getColumnCount()
+ ADDITIONAL_COLUMNS.length];
System.arraycopy(cursor.getColumnNames(), 0, columns, 0,
cursor.getColumnCount());
System.arraycopy(ADDITIONAL_COLUMNS, 0, columns,
cursor.getColumnCount(), ADDITIONAL_COLUMNS.length);
MatrixCursor result = new MatrixCursor(columns);
if (cursor.moveToFirst()) {
do {
RowBuilder builder = result.newRow();
Cursor fileInfo = null;
for (int i = 0; i < cursor.getColumnCount(); i++) {
String data = cursor.getString(i);
builder.add(data);
if (i == colUri)
fileInfo = getContext().getContentResolver().query(
Uri.parse(data), null, null, null, null);
}
if (fileInfo != null) {
if (fileInfo.moveToFirst()) {
builder.add(BaseFileProviderUtils.getFileName(fileInfo));
builder.add(BaseFileProviderUtils.getRealUri(fileInfo)
.toString());
}
fileInfo.close();
}
} while (cursor.moveToNext());
}// if
cursor.close();
return result;
}// appendNameAndRealUri()
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.providers.history;
import group.pals.android.lib.ui.filechooser.BuildConfig;
import group.pals.android.lib.ui.filechooser.R;
import group.pals.android.lib.ui.filechooser.providers.DbUtils;
import java.util.Date;
import android.content.Context;
import android.text.format.DateUtils;
import android.util.Log;
/**
* Utilities for History provider.
*
* @author Hai Bison
* @since v5.1 beta
*/
public class HistoryProviderUtils {
private static final String CLASSNAME = HistoryProviderUtils.class
.getName();
/**
* Checks and cleans up out-dated history items.
*
* @param context
* {@link Context}.
*/
public static void doCleanupOutdatedHistoryItems(Context context) {
if (BuildConfig.DEBUG)
Log.d(CLASSNAME, "doCleanupCache()");
try {
/*
* NOTE: be careful with math, use long values instead of integer
* ones.
*/
final long validityInMillis = new Date().getTime()
- 0;
if (BuildConfig.DEBUG)
Log.d(CLASSNAME, String.format(
"doCleanupCache() - validity = %,d (%s)",
validityInMillis, new Date(validityInMillis)));
context.getContentResolver().delete(
HistoryContract.genContentUri(context),
String.format("%s < '%s'",
HistoryContract.COLUMN_MODIFICATION_TIME,
DbUtils.formatNumber(validityInMillis)), null);
} catch (Throwable t) {
/*
* Currently we just ignore it.
*/
}
}// doCleanupOutdatedHistoryItems()
}

View File

@ -0,0 +1,135 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.providers.localfile;
import group.pals.android.lib.ui.filechooser.BuildConfig;
import group.pals.android.lib.ui.filechooser.utils.Utils;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.FileObserver;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;
/**
* Extended class of {@link FileObserver}, to watch for changes of a directory
* and notify clients of {@link LocalFileProvider} about those changes.
*
* @author Hai Bison
* @since v5.1 beta
*/
public class FileObserverEx extends FileObserver {
private static final String CLASSNAME = FileObserverEx.class.getName();
private static final int FILE_OBSERVER_MASK = FileObserver.CREATE
| FileObserver.DELETE | FileObserver.DELETE_SELF
| FileObserver.MOVE_SELF | FileObserver.MOVED_FROM
| FileObserver.MOVED_TO | FileObserver.ATTRIB | FileObserver.MODIFY;
private static final long MIN_TIME_BETWEEN_EVENTS = 5000;
private static final int MSG_NOTIFY_CHANGES = 0;
/**
* An unknown event, most likely a bug of the system.
*/
private static final int FILE_OBSERVER_UNKNOWN_EVENT = 32768;
private final HandlerThread mHandlerThread = new HandlerThread(CLASSNAME);
private final Handler mHandler;
private long mLastEventTime = SystemClock.elapsedRealtime();
private boolean mWatching = false;
/**
* Creates new instance.
*
* @param context
* the context.
* @param path
* the path to the directory that you want to watch for changes.
*/
public FileObserverEx(final Context context, final String path,
final Uri notificationUri) {
super(path, FILE_OBSERVER_MASK);
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
if (Utils.doLog())
Log.d(CLASSNAME,
String.format(
"mHandler.handleMessage() >> path = '%s' | what = %,d",
path, msg.what));
switch (msg.what) {
case MSG_NOTIFY_CHANGES:
context.getContentResolver().notifyChange(notificationUri,
null);
mLastEventTime = SystemClock.elapsedRealtime();
break;
}
}// handleMessage()
};
}// FileObserverEx()
@Override
public void onEvent(int event, String path) {
/*
* Some bugs of Android...
*/
if (!mWatching || event == FILE_OBSERVER_UNKNOWN_EVENT || path == null
|| mHandler.hasMessages(MSG_NOTIFY_CHANGES)
|| !mHandlerThread.isAlive() || mHandlerThread.isInterrupted())
return;
try {
if (SystemClock.elapsedRealtime() - mLastEventTime <= MIN_TIME_BETWEEN_EVENTS)
mHandler.sendEmptyMessageDelayed(
MSG_NOTIFY_CHANGES,
Math.max(
1,
MIN_TIME_BETWEEN_EVENTS
- (SystemClock.elapsedRealtime() - mLastEventTime)));
else
mHandler.sendEmptyMessage(MSG_NOTIFY_CHANGES);
} catch (Throwable t) {
mWatching = false;
if (Utils.doLog())
Log.e(CLASSNAME, "onEvent() >> " + t);
}
}// onEvent()
@Override
public void startWatching() {
super.startWatching();
if (Utils.doLog())
Log.d(CLASSNAME, String.format("startWatching() >> %s", hashCode()));
mWatching = true;
}// startWatching()
@Override
public void stopWatching() {
super.stopWatching();
if (Utils.doLog())
Log.d(CLASSNAME, String.format("stopWatching() >> %s", hashCode()));
mWatching = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR)
HandlerThreadCompat_v5.quit(mHandlerThread);
mHandlerThread.interrupt();
}// stopWatching()
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.providers.localfile;
import android.os.HandlerThread;
/**
* Helper class for backward compatibility of {@link HandlerThread} from API 5+.
*
* @author Hai Bison
* @since v5.1 beta
*/
public class HandlerThreadCompat_v5 {
/**
* Wrapper for {@link HandlerThread#quit()}.
*
* @param thread
* the handler thread.
*/
public static void quit(HandlerThread thread) {
thread.quit();
}// quit()
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.providers.localfile;
import android.content.Context;
/**
* Contract for local file.
*
* @author Hai Bison
* @since v5.1 beta
*/
public class LocalFileContract {
/**
* The raw authority of this provider.
*/
private static final String AUTHORITY = "android-filechooser.localfile";
/**
* Gets the authority of this provider.
*
* @param context
* the context.
* @return the authority.
*/
public static final String getAuthority(Context context) {
return context.getPackageName() + "." + AUTHORITY;
}// getAuthority()
/**
* The unique ID of this provider.
*/
public static final String _ID = "7dab9818-0a8b-47ef-88cc-10fe538bfaf7";
}

View File

@ -0,0 +1,745 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.providers.localfile;
import group.pals.android.lib.ui.filechooser.BuildConfig;
import group.pals.android.lib.ui.filechooser.R;
import group.pals.android.lib.ui.filechooser.providers.BaseFileProviderUtils;
import group.pals.android.lib.ui.filechooser.providers.ProviderUtils;
import group.pals.android.lib.ui.filechooser.providers.basefile.BaseFileContract.BaseFile;
import group.pals.android.lib.ui.filechooser.providers.basefile.BaseFileProvider;
import group.pals.android.lib.ui.filechooser.utils.FileUtils;
import group.pals.android.lib.ui.filechooser.utils.TextUtils;
import group.pals.android.lib.ui.filechooser.utils.Texts;
import group.pals.android.lib.ui.filechooser.utils.Utils;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.regex.Pattern;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
/**
* Local file provider.
*
* @author Hai Bison
* @since v5.1 beta
*/
public class LocalFileProvider extends BaseFileProvider {
/**
* Used for debugging or something...
*/
private static final String CLASSNAME = LocalFileProvider.class.getName();
private FileObserverEx mFileObserverEx;
@Override
public boolean onCreate() {
BaseFileProviderUtils.registerProviderInfo(LocalFileContract._ID,
LocalFileContract.getAuthority(getContext()));
URI_MATCHER.addURI(LocalFileContract.getAuthority(getContext()),
BaseFile.PATH_DIR + "/*", URI_DIRECTORY);
URI_MATCHER.addURI(LocalFileContract.getAuthority(getContext()),
BaseFile.PATH_FILE + "/*", URI_FILE);
URI_MATCHER.addURI(LocalFileContract.getAuthority(getContext()),
BaseFile.PATH_API, URI_API);
URI_MATCHER.addURI(LocalFileContract.getAuthority(getContext()),
BaseFile.PATH_API + "/*", URI_API_COMMAND);
return true;
}// onCreate()
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
if (Utils.doLog())
Log.d(CLASSNAME, "delete() >> " + uri);
int count = 0;
switch (URI_MATCHER.match(uri)) {
case URI_FILE: {
int taskId = ProviderUtils.getIntQueryParam(uri,
BaseFile.PARAM_TASK_ID, 0);
boolean isRecursive = ProviderUtils.getBooleanQueryParam(uri,
BaseFile.PARAM_RECURSIVE, true);
File file = extractFile(uri);
if (file.canWrite()) {
File parentFile = file.getParentFile();
if (file.isFile() || !isRecursive) {
if (file.delete())
count = 1;
} else {
mMapInterruption.put(taskId, false);
count = deleteFile(taskId, file, isRecursive);
if (mMapInterruption.get(taskId))
if (Utils.doLog())
Log.d(CLASSNAME, "delete() >> cancelled...");
mMapInterruption.delete(taskId);
}
if (count > 0) {
getContext()
.getContentResolver()
.notifyChange(
BaseFile.genContentUriBase(
LocalFileContract
.getAuthority(getContext()))
.buildUpon()
.appendPath(
Uri.fromFile(parentFile)
.toString())
.build(), null);
}
}
break;// URI_FILE
}
default:
throw new IllegalArgumentException("UNKNOWN URI " + uri);
}
if (Utils.doLog())
Log.d(CLASSNAME, "delete() >> count = " + count);
if (count > 0)
getContext().getContentResolver().notifyChange(uri, null);
return count;
}// delete()
@Override
public Uri insert(Uri uri, ContentValues values) {
if (Utils.doLog())
Log.d(CLASSNAME, "insert() >> " + uri);
switch (URI_MATCHER.match(uri)) {
case URI_DIRECTORY:
File file = extractFile(uri);
if (!file.isDirectory() || !file.canWrite())
return null;
File newFile = new File(String.format("%s/%s",
file.getAbsolutePath(),
uri.getQueryParameter(BaseFile.PARAM_NAME)));
switch (ProviderUtils.getIntQueryParam(uri,
BaseFile.PARAM_FILE_TYPE, BaseFile.FILE_TYPE_DIRECTORY)) {
case BaseFile.FILE_TYPE_DIRECTORY:
newFile.mkdir();
break;// FILE_TYPE_DIRECTORY
case BaseFile.FILE_TYPE_FILE:
try {
newFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
break;// FILE_TYPE_FILE
default:
return null;
}
if (newFile.exists()) {
Uri newUri = BaseFile
.genContentIdUriBase(
LocalFileContract.getAuthority(getContext()))
.buildUpon()
.appendPath(Uri.fromFile(newFile).toString()).build();
getContext().getContentResolver().notifyChange(uri, null);
return newUri;
}
return null;// URI_FILE
default:
throw new IllegalArgumentException("UNKNOWN URI " + uri);
}
}// insert()
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
if (Utils.doLog())
Log.d(CLASSNAME, String.format(
"query() >> uri = %s (%s) >> match = %s", uri,
uri.getLastPathSegment(), URI_MATCHER.match(uri)));
switch (URI_MATCHER.match(uri)) {
case URI_API: {
/*
* If there is no command given, return provider ID and name.
*/
MatrixCursor matrixCursor = new MatrixCursor(new String[] {
BaseFile.COLUMN_PROVIDER_ID, BaseFile.COLUMN_PROVIDER_NAME,
BaseFile.COLUMN_PROVIDER_ICON_ATTR });
matrixCursor.newRow().add(LocalFileContract._ID)
.add(getContext().getString(R.string.afc_phone))
.add(R.attr.afc_badge_file_provider_localfile);
return matrixCursor;
}
case URI_API_COMMAND: {
return doAnswerApiCommand(uri);
}// URI_API
case URI_DIRECTORY: {
return doListFiles(uri);
}// URI_DIRECTORY
case URI_FILE: {
return doRetrieveFileInfo(uri);
}// URI_FILE
default:
throw new IllegalArgumentException("UNKNOWN URI " + uri);
}
}// query()
/*
* UTILITIES
*/
/**
* Answers the incoming URI.
*
* @param uri
* the request URI.
* @return the response.
*/
private MatrixCursor doAnswerApiCommand(Uri uri) {
MatrixCursor matrixCursor = null;
if (BaseFile.CMD_CANCEL.equals(uri.getLastPathSegment())) {
int taskId = ProviderUtils.getIntQueryParam(uri,
BaseFile.PARAM_TASK_ID, 0);
synchronized (mMapInterruption) {
if (taskId == 0) {
for (int i = 0; i < mMapInterruption.size(); i++)
mMapInterruption.put(mMapInterruption.keyAt(i), true);
} else if (mMapInterruption.indexOfKey(taskId) >= 0)
mMapInterruption.put(taskId, true);
}
return null;
} else if (BaseFile.CMD_GET_DEFAULT_PATH.equals(uri
.getLastPathSegment())) {
matrixCursor = BaseFileProviderUtils.newBaseFileCursor();
File file = Environment.getExternalStorageDirectory();
if (file == null || !file.isDirectory())
file = new File("/");
int type = file.isFile() ? BaseFile.FILE_TYPE_FILE : (file
.isDirectory() ? BaseFile.FILE_TYPE_DIRECTORY
: BaseFile.FILE_TYPE_UNKNOWN);
RowBuilder newRow = matrixCursor.newRow();
newRow.add(0);// _ID
newRow.add(BaseFile
.genContentIdUriBase(
LocalFileContract.getAuthority(getContext()))
.buildUpon().appendPath(Uri.fromFile(file).toString())
.build().toString());
newRow.add(Uri.fromFile(file).toString());
newRow.add(file.getName());
newRow.add(file.canRead() ? 1 : 0);
newRow.add(file.canWrite() ? 1 : 0);
newRow.add(file.length());
newRow.add(type);
newRow.add(file.lastModified());
newRow.add(FileUtils.getResIcon(type, file.getName()));
}// get default path
else if (BaseFile.CMD_IS_ANCESTOR_OF.equals(uri.getLastPathSegment())) {
return doCheckAncestor(uri);
} else if (BaseFile.CMD_GET_PARENT.equals(uri.getLastPathSegment())) {
File file = new File(Uri.parse(
uri.getQueryParameter(BaseFile.PARAM_SOURCE)).getPath());
file = file.getParentFile();
if (file == null)
return null;
matrixCursor = BaseFileProviderUtils.newBaseFileCursor();
int type = file.isFile() ? BaseFile.FILE_TYPE_FILE : (file
.isDirectory() ? BaseFile.FILE_TYPE_DIRECTORY : (file
.exists() ? BaseFile.FILE_TYPE_UNKNOWN
: BaseFile.FILE_TYPE_NOT_EXISTED));
RowBuilder newRow = matrixCursor.newRow();
newRow.add(0);// _ID
newRow.add(BaseFile
.genContentIdUriBase(
LocalFileContract.getAuthority(getContext()))
.buildUpon().appendPath(Uri.fromFile(file).toString())
.build().toString());
newRow.add(Uri.fromFile(file).toString());
newRow.add(file.getName());
newRow.add(file.canRead() ? 1 : 0);
newRow.add(file.canWrite() ? 1 : 0);
newRow.add(file.length());
newRow.add(type);
newRow.add(file.lastModified());
newRow.add(FileUtils.getResIcon(type, file.getName()));
} else if (BaseFile.CMD_SHUTDOWN.equals(uri.getLastPathSegment())) {
/*
* TODO Stop all tasks. If the activity call this command in
* onDestroy(), it seems that this code block will be suspended and
* started next time the activity starts. So we comment out this.
* Let the Android system do what it wants to do!!!! I hate this.
*/
// synchronized (mMapInterruption) {
// for (int i = 0; i < mMapInterruption.size(); i++)
// mMapInterruption.put(mMapInterruption.keyAt(i), true);
// }
if (mFileObserverEx != null) {
mFileObserverEx.stopWatching();
mFileObserverEx = null;
}
}
return matrixCursor;
}// doAnswerApiCommand()
/**
* Lists the content of a directory, if available.
*
* @param uri
* the URI pointing to a directory.
* @return the content of a directory, or {@code null} if not available.
*/
private MatrixCursor doListFiles(Uri uri) {
MatrixCursor matrixCursor = BaseFileProviderUtils.newBaseFileCursor();
File dir = extractFile(uri);
if (Utils.doLog())
Log.d(CLASSNAME, "srcFile = " + dir);
if (!dir.isDirectory() || !dir.canRead())
return null;
/*
* Prepare params...
*/
int taskId = ProviderUtils.getIntQueryParam(uri,
BaseFile.PARAM_TASK_ID, 0);
boolean showHiddenFiles = ProviderUtils.getBooleanQueryParam(uri,
BaseFile.PARAM_SHOW_HIDDEN_FILES);
boolean sortAscending = ProviderUtils.getBooleanQueryParam(uri,
BaseFile.PARAM_SORT_ASCENDING, true);
int sortBy = ProviderUtils.getIntQueryParam(uri,
BaseFile.PARAM_SORT_BY, BaseFile.SORT_BY_NAME);
int filterMode = ProviderUtils.getIntQueryParam(uri,
BaseFile.PARAM_FILTER_MODE,
BaseFile.FILTER_FILES_AND_DIRECTORIES);
int limit = ProviderUtils.getIntQueryParam(uri, BaseFile.PARAM_LIMIT,
1000);
String positiveRegex = uri
.getQueryParameter(BaseFile.PARAM_POSITIVE_REGEX_FILTER);
String negativeRegex = uri
.getQueryParameter(BaseFile.PARAM_NEGATIVE_REGEX_FILTER);
mMapInterruption.put(taskId, false);
boolean[] hasMoreFiles = { false };
List<File> files = new ArrayList<File>();
listFiles(taskId, dir, showHiddenFiles, filterMode, limit,
positiveRegex, negativeRegex, files, hasMoreFiles);
if (!mMapInterruption.get(taskId)) {
sortFiles(taskId, files, sortAscending, sortBy);
if (!mMapInterruption.get(taskId)) {
for (int i = 0; i < files.size(); i++) {
if (mMapInterruption.get(taskId))
break;
File f = files.get(i);
int type = f.isFile() ? BaseFile.FILE_TYPE_FILE : (f
.isDirectory() ? BaseFile.FILE_TYPE_DIRECTORY
: BaseFile.FILE_TYPE_UNKNOWN);
RowBuilder newRow = matrixCursor.newRow();
newRow.add(i);// _ID
newRow.add(BaseFile
.genContentIdUriBase(
LocalFileContract
.getAuthority(getContext()))
.buildUpon().appendPath(Uri.fromFile(f).toString())
.build().toString());
newRow.add(Uri.fromFile(f).toString());
newRow.add(f.getName());
newRow.add(f.canRead() ? 1 : 0);
newRow.add(f.canWrite() ? 1 : 0);
newRow.add(f.length());
newRow.add(type);
newRow.add(f.lastModified());
newRow.add(FileUtils.getResIcon(type, f.getName()));
}// for files
/*
* The last row contains:
*
* - The ID;
*
* - The base file URI to original directory, which has
* parameter BaseFile.PARAM_HAS_MORE_FILES to indicate the
* directory has more files or not.
*
* - The system absolute path to original directory.
*
* - The name of original directory.
*/
RowBuilder newRow = matrixCursor.newRow();
newRow.add(files.size());// _ID
newRow.add(BaseFile
.genContentIdUriBase(
LocalFileContract.getAuthority(getContext()))
.buildUpon()
.appendPath(Uri.fromFile(dir).toString())
.appendQueryParameter(BaseFile.PARAM_HAS_MORE_FILES,
Boolean.toString(hasMoreFiles[0])).build()
.toString());
newRow.add(Uri.fromFile(dir).toString());
newRow.add(dir.getName());
}
}
try {
if (mMapInterruption.get(taskId)) {
if (Utils.doLog())
Log.d(CLASSNAME, "query() >> cancelled...");
return null;
}
} finally {
mMapInterruption.delete(taskId);
}
if (mFileObserverEx != null)
mFileObserverEx.stopWatching();
mFileObserverEx = new FileObserverEx(getContext(),
dir.getAbsolutePath(), uri);
mFileObserverEx.startWatching();
/*
* Tells the Cursor what URI to watch, so it knows when its source data
* changes.
*/
matrixCursor.setNotificationUri(getContext().getContentResolver(), uri);
return matrixCursor;
}// doListFiles()
/**
* Retrieves file information of a single file.
*
* @param uri
* the URI pointing to a file.
* @return the file information. Can be {@code null}, based on the input
* parameters.
*/
private MatrixCursor doRetrieveFileInfo(Uri uri) {
MatrixCursor matrixCursor = BaseFileProviderUtils.newBaseFileCursor();
File file = extractFile(uri);
int type = file.isFile() ? BaseFile.FILE_TYPE_FILE : (file
.isDirectory() ? BaseFile.FILE_TYPE_DIRECTORY
: (file.exists() ? BaseFile.FILE_TYPE_UNKNOWN
: BaseFile.FILE_TYPE_NOT_EXISTED));
RowBuilder newRow = matrixCursor.newRow();
newRow.add(0);// _ID
newRow.add(BaseFile
.genContentIdUriBase(
LocalFileContract.getAuthority(getContext()))
.buildUpon().appendPath(Uri.fromFile(file).toString()).build()
.toString());
newRow.add(Uri.fromFile(file).toString());
newRow.add(file.getName());
newRow.add(file.canRead() ? 1 : 0);
newRow.add(file.canWrite() ? 1 : 0);
newRow.add(file.length());
newRow.add(type);
newRow.add(file.lastModified());
newRow.add(FileUtils.getResIcon(type, file.getName()));
return matrixCursor;
}// doRetrieveFileInfo()
/**
* Lists all file inside {@code dir}.
*
* @param taskId
* the task ID.
* @param dir
* the source directory.
* @param showHiddenFiles
* {@code true} or {@code false}.
* @param filterMode
* can be one of {@link BaseFile#FILTER_DIRECTORIES_ONLY},
* {@link BaseFile#FILTER_FILES_ONLY},
* {@link BaseFile#FILTER_FILES_AND_DIRECTORIES}.
* @param limit
* the limit.
* @param positiveRegex
* the positive regex filter.
* @param negativeRegex
* the negative regex filter.
* @param results
* the results.
* @param hasMoreFiles
* the first item will contain a value representing that there is
* more files (exceeding {@code limit}) or not.
*/
private void listFiles(final int taskId, final File dir,
final boolean showHiddenFiles, final int filterMode,
final int limit, String positiveRegex, String negativeRegex,
final List<File> results, final boolean hasMoreFiles[]) {
final Pattern positivePattern = Texts.compileRegex(positiveRegex);
final Pattern negativePattern = Texts.compileRegex(negativeRegex);
hasMoreFiles[0] = false;
try {
dir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
if (mMapInterruption.get(taskId))
throw new CancellationException();
final boolean isFile = pathname.isFile();
final String name = pathname.getName();
/*
* Filters...
*/
if (filterMode == BaseFile.FILTER_DIRECTORIES_ONLY
&& isFile)
return false;
if (!showHiddenFiles && name.startsWith("."))
return false;
if (isFile && positivePattern != null
&& !positivePattern.matcher(name).find())
return false;
if (isFile && negativePattern != null
&& negativePattern.matcher(name).find())
return false;
/*
* Limit...
*/
if (results.size() >= limit) {
hasMoreFiles[0] = true;
throw new CancellationException("Exceeding limit...");
}
results.add(pathname);
return false;
}// accept()
});
} catch (CancellationException e) {
if (Utils.doLog())
Log.d(CLASSNAME, "listFiles() >> cancelled... >> " + e);
}
}// listFiles()
/**
* Sorts {@code files}.
*
* @param taskId
* the task ID.
* @param files
* list of files.
* @param ascending
* {@code true} or {@code false}.
* @param sortBy
* can be one of {@link BaseFile.#_SortByModificationTime},
* {@link BaseFile.#_SortByName}, {@link BaseFile.#_SortBySize}.
*/
private void sortFiles(final int taskId, final List<File> files,
final boolean ascending, final int sortBy) {
try {
Collections.sort(files, new Comparator<File>() {
@Override
public int compare(File lhs, File rhs) {
if (mMapInterruption.get(taskId))
throw new CancellationException();
if (lhs.isDirectory() && !rhs.isDirectory())
return -1;
if (!lhs.isDirectory() && rhs.isDirectory())
return 1;
/*
* Default is to compare by name (case insensitive).
*/
int res = mCollator.compare(lhs.getName(), rhs.getName());
switch (sortBy) {
case BaseFile.SORT_BY_NAME:
break;// SortByName
case BaseFile.SORT_BY_SIZE:
if (lhs.length() > rhs.length())
res = 1;
else if (lhs.length() < rhs.length())
res = -1;
break;// SortBySize
case BaseFile.SORT_BY_MODIFICATION_TIME:
if (lhs.lastModified() > rhs.lastModified())
res = 1;
else if (lhs.lastModified() < rhs.lastModified())
res = -1;
break;// SortByDate
}
return ascending ? res : -res;
}// compare()
});
} catch (CancellationException e) {
if (Utils.doLog())
Log.d(CLASSNAME, "sortFiles() >> cancelled...");
}
}// sortFiles()
/**
* Deletes {@code file}.
*
* @param taskId
* the task ID.
* @param file
* {@link File}.
* @param recursive
* if {@code true} and {@code file} is a directory, this thread
* will delete all sub files/ folders of it recursively.
* @return the total files deleted.
*/
private int deleteFile(final int taskId, final File file,
final boolean recursive) {
final int[] count = { 0 };
if (mMapInterruption.get(taskId))
return count[0];
if (file.isFile()) {
if (file.delete())
count[0]++;
return count[0];
}
/*
* If the directory is empty, try to delete it and return here.
*/
if (file.delete()) {
count[0]++;
return count[0];
}
if (!recursive)
return count[0];
try {
try {
file.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
if (mMapInterruption.get(taskId))
throw new CancellationException();
if (pathname.isFile()) {
if (pathname.delete())
count[0]++;
} else if (pathname.isDirectory()) {
if (recursive)
count[0] += deleteFile(taskId, pathname,
recursive);
else if (pathname.delete())
count[0]++;
}
return false;
}// accept()
});
} catch (CancellationException e) {
return count[0];
}
if (file.delete())
count[0]++;
} catch (Throwable t) {
// TODO
}
return count[0];
}// deleteFile()
/**
* Checks ancestor with {@link BaseFile#CMD_IS_ANCESTOR_OF},
* {@link BaseFile#PARAM_SOURCE} and {@link BaseFile#PARAM_TARGET}.
*
* @param uri
* the original URI from client.
* @return {@code null} if source is not ancestor of target; or a
* <i>non-null but empty</i> cursor if the source is.
*/
private MatrixCursor doCheckAncestor(Uri uri) {
File source = new File(Uri.parse(
uri.getQueryParameter(BaseFile.PARAM_SOURCE)).getPath());
File target = new File(Uri.parse(
uri.getQueryParameter(BaseFile.PARAM_TARGET)).getPath());
if (source == null || target == null)
return null;
boolean validate = ProviderUtils.getBooleanQueryParam(uri,
BaseFile.PARAM_VALIDATE, true);
if (validate) {
if (!source.isDirectory() || !target.exists())
return null;
}
if (source.equals(target.getParentFile())
|| (target.getParent() != null && target.getParent()
.startsWith(source.getAbsolutePath())))
return BaseFileProviderUtils.newClosedCursor();
return null;
}// doCheckAncestor()
/**
* Extracts source file from request URI.
*
* @param uri
* the original URI.
* @return the file.
*/
private static File extractFile(Uri uri) {
String fileName = Uri.parse(uri.getLastPathSegment()).getPath();
if (uri.getQueryParameter(BaseFile.PARAM_APPEND_PATH) != null)
fileName += Uri.parse(
uri.getQueryParameter(BaseFile.PARAM_APPEND_PATH))
.getPath();
if (uri.getQueryParameter(BaseFile.PARAM_APPEND_NAME) != null)
fileName += "/" + uri.getQueryParameter(BaseFile.PARAM_APPEND_NAME);
if (Utils.doLog())
Log.d(CLASSNAME, "extractFile() >> " + fileName);
return new File(fileName);
}// extractFile()
}

View File

@ -0,0 +1,475 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.ui.widget;
import group.pals.android.lib.ui.filechooser.BuildConfig;
import group.pals.android.lib.ui.filechooser.R;
import group.pals.android.lib.ui.filechooser.utils.Utils;
import group.pals.android.lib.ui.filechooser.utils.ui.Ui;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Handler;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* AFC Search view.
*
* @author Hai Bison
* @since v5.1 beta
*/
public class AfcSearchView extends LinearLayout {
private static final String CLASSNAME = AfcSearchView.class.getName();
/**
* Callbacks for changes to the query text.
*/
public static interface OnQueryTextListener {
/**
* Called when the user submits the query. This could be due to a key
* press on the keyboard or due to pressing a submit button.
* <p>
* <b>Note:</b> This method is called before setting the new search
* query to last search query (which can be obtained with
* {@link AfcSearchView#getSearchText()}).
* </p>
*
* @param query
* the query text that is to be submitted.
*/
void onQueryTextSubmit(String query);
}// OnQueryTextListener
public static interface OnStateChangeListener {
/**
* The user is attempting to open the SearchView.
*/
void onOpen();
/**
* The user is attempting to close the SearchView.
*/
void onClose();
}// OnStateChangeListener
/*
* CONTROLS
*/
private final View mButtonSearch;
private final EditText mTextSearch;
private final View mButtonClear;
/*
* FIELDS
*/
private int mDelayTimeSubmission;
private boolean mIconified;
private boolean mClosable;
private CharSequence mSearchText;
/*
* LISTENERS
*/
private OnQueryTextListener mOnQueryTextListener;
private OnStateChangeListener mOnStateChangeListener;
/**
* Creates new instance.
*
* @param context
* {@link Context}.
*/
public AfcSearchView(Context context) {
this(context, null);
}// AfcSearchView()
/**
* Creates new instance.
*
* @param context
* {@link Context}.
* @param attrs
* {@link AttributeSet}.
*/
public AfcSearchView(Context context, AttributeSet attrs) {
super(context, attrs);
/*
* LOADS LAYOUTS
*/
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.afc_widget_search_view, this, true);
mButtonSearch = findViewById(R.id.afc_widget_search_view_button_search);
mTextSearch = (EditText) findViewById(R.id.afc_widget_search_view_textview_search);
mButtonClear = findViewById(R.id.afc_widget_search_view_button_clear);
/*
* ASSIGNS LISTENERS & ATTRIBUTES
*/
mButtonSearch.setOnClickListener(mButtonSearchOnClickListener);
mTextSearch.addTextChangedListener(mTextSearchTextWatcher);
mTextSearch.setOnKeyListener(mTextSearchOnKeyListener);
mTextSearch
.setOnEditorActionListener(mTextSearchOnEditorActionListener);
mButtonClear.setOnClickListener(mButtonClearOnClickListener);
/*
* LOADS ATTRIBUTES
*/
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.AfcSearchView);
setDelayTimeSubmission(a.getInt(
R.styleable.AfcSearchView_delayTimeSubmission, 0));
updateViewsVisibility(
a.getBoolean(R.styleable.AfcSearchView_iconified, true), false);
setClosable(a.getBoolean(R.styleable.AfcSearchView_closable, true));
setEnabled(a.getBoolean(R.styleable.AfcSearchView_enabled, true));
mTextSearch.setHint(a.getString(R.styleable.AfcSearchView_hint));
a.recycle();
}// AfcSearchView()
/**
* Gets the search text.
*
* @return the search text, can be {@code null}.
*/
public CharSequence getSearchText() {
return mSearchText;
}// getSearchText()
/**
* Gets delay time submission. This is the time that after the user entered
* a search term and waited for, then the handler will be invoked to process
* that search term.
*
* @return the delay time, in milliseconds.
* @see #setDelayTimeSubmission(int)
*/
public int getDelayTimeSubmission() {
return mDelayTimeSubmission;
}// getDelayTimeSubmission()
/**
* Sets delay time submission. This is the time that after the user entered
* a search term and waited for, then the handler will be invoked to process
* that search term.
*
* @param millis
* delay time, in milliseconds. If {@code <= 0}, auto-submission
* will be disabled.
* @see #getDelayTimeSubmission()
*/
public void setDelayTimeSubmission(int millis) {
if (mDelayTimeSubmission != millis) {
mDelayTimeSubmission = Math.max(0, millis);
if (mDelayTimeSubmission <= 0)
mAutoSubmissionHandler.removeCallbacksAndMessages(null);
}
}// setDelayTimeSubmission()
/**
* Checks if this search view is iconfied or not.
*
* @return {@code true} or {@code false}.
* @see #close()
* @see #open()
*/
public boolean isIconified() {
return mIconified;
}// isIconfied()
/**
* Updates views visibility.
*
* @param collapsed
* {@code true} or {@code false}.
* @param showSoftKeyboard
* set to {@code true} if you want to force show the soft
* keyboard in <i>expanded</i> state.
* @see #isIconified()
*/
protected void updateViewsVisibility(boolean collapsed,
boolean showSoftKeyboard) {
if (Utils.doLog())
Log.d(CLASSNAME, "updateViewsVisibility() >> " + collapsed);
mIconified = collapsed;
/*
* Always remove this trap first...
*/
if (mIconified)
mAutoSubmissionHandler.removeCallbacksAndMessages(null);
if (getOnStateChangeListener() != null)
if (mIconified)
getOnStateChangeListener().onClose();
else
getOnStateChangeListener().onOpen();
mTextSearch.setVisibility(mIconified ? GONE : VISIBLE);
if (mIconified) {
mSearchText = null;
mTextSearch.removeTextChangedListener(mTextSearchTextWatcher);
mTextSearch.setText(null);
mTextSearch.setFocusable(false);
mTextSearch.setFocusableInTouchMode(false);
mTextSearch.clearFocus();
setEnabled(false);
Ui.showSoftKeyboard(mTextSearch, false);
} else {
mTextSearch.addTextChangedListener(mTextSearchTextWatcher);
mTextSearch.setFocusable(true);
mTextSearch.setFocusableInTouchMode(true);
if (showSoftKeyboard) {
mTextSearch.requestFocus();
Ui.showSoftKeyboard(mTextSearch, true);
}
setEnabled(true);
}
}// updateViewsVisibility()
/**
* Minimizes this search view. Does nothing if this search view is not
* closable.
*
* @see #isIconified()
* @see #isClosable()
* @see #open()
*/
public void close() {
if (isClosable() && !isIconified())
updateViewsVisibility(true, true);
}// close()
/**
* Maximizes the view, lets the user to be able to enter search term.
*
* @see #close()
* @see #isIconified()
*/
public void open() {
if (isIconified())
updateViewsVisibility(false, true);
}// open()
/**
* Checks if this search view is closable or not.
*
* @return {@code true} or {@code false}.
*/
public boolean isClosable() {
return mClosable;
}
/**
* Sets closable.
*
* @param closable
* {@code true} or {@code false}.
*/
public void setClosable(boolean closable) {
mClosable = closable;
if (mClosable)
mButtonClear.setVisibility(VISIBLE);
}
/**
* Sets the query text listener.
*
* @param listener
* {@link OnQueryTextListener}.
* @see #getOnQueryTextListener()
*/
public void setOnQueryTextListener(OnQueryTextListener listener) {
mOnQueryTextListener = listener;
}
/**
* Gets the on query text listener.
*
* @return {@link OnQueryTextListener}, can be {@code null}.
* @see #setOnQueryTextListener(OnQueryTextListener)
*/
public OnQueryTextListener getOnQueryTextListener() {
return mOnQueryTextListener;
}
/**
* Sets on close listener.
*
* @param listener
* {@link OnClickListener}.
* @see #getOnStateChangeListener()
*/
public void setOnStateChangeListener(OnStateChangeListener listener) {
mOnStateChangeListener = listener;
}
/**
* Gets on close listener.
*
* @return {@link OnStateChangeListener}, can be {@code null}.
* @see #setOnStateChangeListener(OnStateChangeListener)
*/
public OnStateChangeListener getOnStateChangeListener() {
return mOnStateChangeListener;
}
@Override
public void setEnabled(boolean enabled) {
if (isEnabled() == enabled)
return;
for (View v : new View[] { mButtonSearch, mTextSearch, mButtonClear })
v.setEnabled(enabled);
super.setEnabled(enabled);
}// setEnabled()
/*
* LISTENERS
*/
private final View.OnClickListener mButtonSearchOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (isIconified()) {
updateViewsVisibility(false, false);
} else {
mAutoSubmissionHandler.removeCallbacksAndMessages(null);
if (getOnQueryTextListener() != null)
getOnQueryTextListener().onQueryTextSubmit(
mTextSearch.getText().toString());
mSearchText = mTextSearch.getText();
}
}// onClick()
};// mButtonSearchOnClickListener
private final Handler mAutoSubmissionHandler = new Handler();
private final Runnable mAutoSubmissionRunnable = new Runnable() {
@Override
public void run() {
if (Utils.doLog())
Log.d(CLASSNAME, "mAutoSubmissionRunnable.run()");
mButtonSearch.performClick();
}// run()
};// mAutoSubmissionRunnable
private final TextWatcher mTextSearchTextWatcher = new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
/*
* Do nothing.
*/
}// onTextChanged()
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
if (Utils.doLog())
Log.d(CLASSNAME, "beforeTextChanged()");
mAutoSubmissionHandler.removeCallbacksAndMessages(null);
}// beforeTextChanged()
@Override
public void afterTextChanged(Editable s) {
if (Utils.doLog())
Log.d(CLASSNAME,
"afterTextChanged() >>> delayTimeSubmission = "
+ getDelayTimeSubmission());
if (TextUtils.isEmpty(mTextSearch.getText())) {
if (!isClosable())
mButtonClear.setVisibility(GONE);
} else
mButtonClear.setVisibility(VISIBLE);
if (getDelayTimeSubmission() > 0)
mAutoSubmissionHandler.postDelayed(mAutoSubmissionRunnable,
getDelayTimeSubmission());
}// afterTextChanged()
};// mTextSearchTextWatcher
private final View.OnKeyListener mTextSearchOnKeyListener = new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_UP) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
mButtonSearch.performClick();
return true;
case KeyEvent.KEYCODE_ESCAPE:
mButtonClear.performClick();
return true;
}
}
return false;
}// onKey()
};// mTextSearchOnKeyListener
private final TextView.OnEditorActionListener mTextSearchOnEditorActionListener = new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
mButtonSearch.performClick();
return true;
}
return false;
}// onEditorAction()
};// mTextSearchOnEditorActionListener
private final View.OnClickListener mButtonClearOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (TextUtils.isEmpty(mTextSearch.getText()))
close();
else
mTextSearch.setText(null);
}// onClick()
};// mButtonClearOnClickListener
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils;
/**
* The converter.
*
* @author Hai Bison
*
*/
public class Converter {
/**
* Converts {@code size} (in bytes) to string. This tip is from:
* {@code http://stackoverflow.com/a/5599842/942821}.
*
* @param size
* the size in bytes.
* @return e.g.:
* <p/>
* <ul>
* <li>128 B</li>
* <li>1.5 KiB</li>
* <li>10 MiB</li>
* <li>...</li>
* </ul>
*/
public static String sizeToStr(double size) {
if (size <= 0)
return "0 B";
final String[] units = { "", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi",
"Yi" };
final short blockSize = 1024;
int digitGroups = (int) (Math.log10(size) / Math.log10(blockSize));
if (digitGroups >= units.length)
digitGroups = units.length - 1;
size = size / Math.pow(blockSize, digitGroups);
return String.format(
String.format("%s %%sB", digitGroups == 0 ? "%,.0f" : "%,.2f"),
size, units[digitGroups]);
}// sizeToStr()
}

View File

@ -0,0 +1,119 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils;
import group.pals.android.lib.ui.filechooser.R;
import group.pals.android.lib.ui.filechooser.prefs.DisplayPrefs.FileTimeDisplay;
import java.util.Calendar;
import android.content.Context;
/**
* Date utilities.
*
* @author Hai Bison
* @since v4.7 beta
*/
public class DateUtils {
/**
* Used with format methods of {@link android.text.format.DateUtils}. For
* example: "10:01 AM".
*/
@SuppressWarnings("deprecation")
public static final int FORMAT_SHORT_TIME = android.text.format.DateUtils.FORMAT_12HOUR
| android.text.format.DateUtils.FORMAT_SHOW_TIME;
/**
* Used with format methods of {@link android.text.format.DateUtils}. For
* example: "Oct 01".
*/
public static final int FORMAT_MONTH_AND_DAY = android.text.format.DateUtils.FORMAT_ABBREV_MONTH
| android.text.format.DateUtils.FORMAT_SHOW_DATE
| android.text.format.DateUtils.FORMAT_NO_YEAR;
/**
* Used with format methods of {@link android.text.format.DateUtils}. For
* example: "2012".
*/
public static final int FORMAT_YEAR = android.text.format.DateUtils.FORMAT_SHOW_YEAR;
/**
* Formats date.
*
* @param context
* {@link Context}.
* @param millis
* time in milliseconds.
* @param fileTimeDisplay
* {@link FileTimeDisplay}.
* @return the formatted string
*/
public static String formatDate(Context context, long millis,
FileTimeDisplay fileTimeDisplay) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(millis);
return formatDate(context, cal, fileTimeDisplay);
}// formatDate()
/**
* Formats date.
*
* @param context
* {@link Context}.
* @param date
* {@link Calendar}.
* @param fileTimeDisplay
* {@link FileTimeDisplay}.
* @return the formatted string, for local human reading.
*/
public static String formatDate(Context context, Calendar date,
FileTimeDisplay fileTimeDisplay) {
final Calendar yesterday = Calendar.getInstance();
yesterday.add(Calendar.DAY_OF_YEAR, -1);
String res;
if (android.text.format.DateUtils.isToday(date.getTimeInMillis())) {
res = android.text.format.DateUtils.formatDateTime(context,
date.getTimeInMillis(), FORMAT_SHORT_TIME);
}// today
else if (date.get(Calendar.YEAR) == yesterday.get(Calendar.YEAR)
&& date.get(Calendar.DAY_OF_YEAR) == yesterday
.get(Calendar.DAY_OF_YEAR)) {
res = String.format(
"%s, %s",
context.getString(R.string.afc_yesterday),
android.text.format.DateUtils.formatDateTime(context,
date.getTimeInMillis(), FORMAT_SHORT_TIME));
}// yesterday
else if (date.get(Calendar.YEAR) == yesterday.get(Calendar.YEAR)) {
if (fileTimeDisplay.showTimeForOldDaysThisYear)
res = android.text.format.DateUtils.formatDateTime(context,
date.getTimeInMillis(), FORMAT_SHORT_TIME
| FORMAT_MONTH_AND_DAY);
else
res = android.text.format.DateUtils.formatDateTime(context,
date.getTimeInMillis(), FORMAT_MONTH_AND_DAY);
}// this year
else {
if (fileTimeDisplay.showTimeForOldDays)
res = android.text.format.DateUtils.formatDateTime(context,
date.getTimeInMillis(), FORMAT_SHORT_TIME
| FORMAT_MONTH_AND_DAY | FORMAT_YEAR);
else
res = android.text.format.DateUtils.formatDateTime(context,
date.getTimeInMillis(), FORMAT_MONTH_AND_DAY
| FORMAT_YEAR);
}// other years (maybe older or newer than this year)
return res;
}// formatDate()
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils;
import group.pals.android.lib.ui.filechooser.R;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.view.Window;
import android.widget.TextView;
/**
* Something funny :-)
*
* @author Hai Bison
*/
public class E {
/**
* Shows it!
*
* @param context
* {@link Context}
*/
public static void show(Context context) {
String msg = null;
try {
msg = String.format("Hi :-)\n\n" + "%s v%s\n"
+ "…by Hai Bison Apps\n\n" + "http://www.haibison.com\n\n"
+ "Hope you enjoy this library.", Sys.LIB_NAME,
Sys.LIB_VERSION_NAME);
} catch (Exception e) {
msg = "Oops… You've found a broken Easter egg, try again later :-(";
}
final Context ctw = new ContextThemeWrapper(context,
R.style.Afc_Theme_Dialog_Dark);
final int padding = ctw.getResources().getDimensionPixelSize(
R.dimen.afc_10dp);
TextView textView = new TextView(ctw);
textView.setText(msg);
textView.setPadding(padding, padding, padding, padding);
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
ctw.startActivity(new Intent(Intent.ACTION_VIEW, Uri
.parse("http://www.haibison.com")));
} catch (Throwable t) {
/*
* Ignore it.
*/
}
}// onClick()
});
Dialog dialog = new Dialog(ctw, R.style.Afc_Theme_Dialog_Dark);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setCanceledOnTouchOutside(true);
dialog.setContentView(textView);
dialog.show();
}// show()
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils;
/**
* Environment utilities :-)
*
* @author Hai Bison
* @since v5.1 beta
*/
public class EnvUtils {
/**
* The starting ID. This is used to calculate next unique ID in a session.
*/
private static int mId = 0;
/**
* Generates a unique ID (in a working session).
*
* @return the UID.
*/
public static final int genId() {
return mId++;
}// genId()
}

View File

@ -0,0 +1,99 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils;
import group.pals.android.lib.ui.filechooser.R;
import group.pals.android.lib.ui.filechooser.providers.basefile.BaseFileContract.BaseFile;
import java.util.regex.Pattern;
import android.util.SparseArray;
/**
* Utilities for files.
*
* @author Hai Bison
* @since v4.3 beta
*/
public class FileUtils {
/**
* Map of the pattern for file types corresponding to resource IDs for
* icons.
*/
private static final SparseArray<Pattern> MAP_FILE_ICONS = new SparseArray<Pattern>();
static {
MAP_FILE_ICONS.put(R.drawable.afc_file_audio,
Pattern.compile(MimeTypes.REGEX_FILE_TYPE_AUDIOS));
MAP_FILE_ICONS.put(R.drawable.afc_file_video,
Pattern.compile(MimeTypes.REGEX_FILE_TYPE_VIDEOS));
MAP_FILE_ICONS.put(R.drawable.afc_file_image,
Pattern.compile(MimeTypes.REGEX_FILE_TYPE_IMAGES));
MAP_FILE_ICONS.put(R.drawable.afc_file_plain_text,
Pattern.compile(MimeTypes.REGEX_FILE_TYPE_PLAIN_TEXTS));
MAP_FILE_ICONS.put(R.drawable.afc_file_kp2a,
Pattern.compile(MimeTypes.REGEX_FILE_TYPE_KEEPASS2ANDROID));
/*
* APK files are counted before compressed files.
*/
MAP_FILE_ICONS.put(R.drawable.afc_file_apk,
Pattern.compile(MimeTypes.REGEX_FILE_TYPE_APKS));
MAP_FILE_ICONS.put(R.drawable.afc_file_compressed,
Pattern.compile(MimeTypes.REGEX_FILE_TYPE_COMPRESSED));
}
/**
* Gets resource icon based on file type and name.
*
* @param fileType
* the file type, can be one of
* {@link BaseFile#FILE_TYPE_DIRECTORY},
* {@link BaseFile#FILE_TYPE_FILE},
* {@link BaseFile#FILE_TYPE_UNKNOWN}.
* @param fileName
* the file name.
* @return the resource icon ID.
*/
public static int getResIcon(int fileType, String fileName) {
switch (fileType) {
case BaseFile.FILE_TYPE_DIRECTORY: {
return R.drawable.afc_folder;
}// FILE_TYPE_DIRECTORY
case BaseFile.FILE_TYPE_FILE: {
for (int i = 0; i < MAP_FILE_ICONS.size(); i++)
if (MAP_FILE_ICONS.valueAt(i).matcher(fileName).find())
return MAP_FILE_ICONS.keyAt(i);
return R.drawable.afc_file;
}// FILE_TYPE_FILE
default:
return android.R.drawable.ic_delete;
}
}// getResIcon()
/**
* Checks whether the filename given is valid or not.
* <p/>
* See <a href="http://en.wikipedia.org/wiki/Filename">wiki</a> for more
* information.
*
* @param name
* name of the file
* @return {@code true} if the {@code name} is valid, and vice versa (if it
* contains invalid characters or it is {@code null}/ empty)
*/
public static boolean isFilenameValid(String name) {
return name != null && name.trim().matches("[^\\\\/?%*:|\"<>]+");
}// isFilenameValid()
}

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils;
/**
* Mime types for files.
*
* @author Hai Bison
* @since v4.5 beta
*/
public class MimeTypes {
/**
* Regular expression for plain text files.
*/
public static final String REGEX_FILE_TYPE_PLAIN_TEXTS = "(?si)^.+\\.(txt|"
+ "html?|json|csv|java|pas|php.*|c|cpp|bas|python|js|javascript|"
+ "scala|xml|kml|css|ps|xslt?|tpl|tsv|bash|cmd|pl|pm|ps1|ps1xml|"
+ "psc1|psd1|psm1|py|pyc|pyo|r|rb|sdl|sh|tcl|vbs|xpl|ada|adb|ads|"
+ "clj|cls|cob|cbl|cxx|cs|csproj|d|e|el|go|h|hpp|hxx|l|m|url|ini|"
+ "prop|conf|properties|rc|srt|sa?mi|cmml|lrc)$";
/**
* Regular expression for files supported by Keepass2Android.
*/
public static final String REGEX_FILE_TYPE_KEEPASS2ANDROID = "(?si)^.+\\.(kdbx|kdbp)$";
/**
* Regular expression for HTML files.
*/
public static final String REGEX_FILE_TYPE_HTMLS = "(?si)^.+\\.(html?)$";
/**
* Regular expression for image files.
*
* @see http://en.wikipedia.org/wiki/Image_file_formats
*/
public static final String REGEX_FILE_TYPE_IMAGES = "(?si)^.+\\.(gif|jpe?g|"
+ "png|tiff?|wmf|emf|jfif|exif|raw|bmp|ppm|pgm|pbm|pnm|webp|riff|"
+ "tga|ilbm|img|pcx|ecw|sid|cd5|fits|pgf|xcf|svg|pns|jps|icon?|"
+ "jp2|mng|xpm|djvu)$";
/**
* Regular expression for audio files.
*
* @see http://en.wikipedia.org/wiki/Audio_file_format
* @see http://en.wikipedia.org/wiki/List_of_file_formats
*/
public static final String REGEX_FILE_TYPE_AUDIOS = "(?si)^.+\\.(mp[2-3]+|"
+ "wav|aiff|au|m4a|ogg|raw|flac|mid|amr|aac|alac|atrac|awb|m4p|"
+ "mmf|mpc|ra|rm|tta|vox|wma)$";
/**
* Regular expression for video files.
*
* @see http://en.wikipedia.org/wiki/Video_file_formats
*/
public static final String REGEX_FILE_TYPE_VIDEOS = "(?si)^.+\\.(mp[4]+|"
+ "flv|wmv|webm|m4v|3gp|mkv|mov|mpe?g|rmv?|ogv|avi)$";
/**
* Regular expression for APK files.
*/
public static final String REGEX_FILE_TYPE_APKS = "(?si)^.+\\.apk$";
/**
* Regular expression for compressed files.
*
* @see http://en.wikipedia.org/wiki/List_of_file_formats
*/
public static final String REGEX_FILE_TYPE_COMPRESSED = "(?si)^.+\\.(zip|"
+ "7z|lz?|[jrt]ar|gz|gzip|bzip|xz|cab|sfx|z|iso|bz?|rz|s7z|apk|"
+ "dmg)$";
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils;
/**
* System variables.
*
* @author Hai Bison
*
*/
public class Sys {
/**
* The library name.
*/
public static final String LIB_NAME = "android-filechooser";
/**
* The library version name.
*/
public static final String LIB_VERSION_NAME = "5.4.4 beta";
/**
* The library version code.
*/
public static final int LIB_VERSION_CODE = 56;
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils;
import java.util.regex.Pattern;
/**
* Text utilities.
*
* @author Hai Bison
* @since v4.3 beta
*/
public class TextUtils {
/**
* Quotes a text in double quotation mark.
*
* @param s
* the text, if {@code null}, empty string will be used
* @return the quoted text
*/
public static String quote(String s) {
return String.format("\"%s\"", s != null ? s : "");
}// quote()
/**
* Compiles {@code regex}.
*
* @param regex
* the regex.
* @return a compiled {@link Pattern}, or {@code null} if there is an error
* while compiling.
*/
public static Pattern compileRegex(String regex) {
if (android.text.TextUtils.isEmpty(regex))
return null;
try {
return Pattern.compile(regex);
} catch (Throwable t) {
return null;
}
}// compileRegex()
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils;
import java.util.regex.Pattern;
/**
* Text utilities.
*
* @author Hai Bison
* @since v4.3 beta
*/
public class Texts {
/**
* The period "."
*/
public static final char C_PERIOD = '.';
/**
* Quotes a text in double quotation mark.
*
* @param s
* the text, if {@code null}, empty string will be used
* @return the quoted text
*/
public static String quote(String s) {
return String.format("\"%s\"", s != null ? s : "");
}// quote()
/**
* Compiles {@code regex}.
*
* @param regex
* the regex.
* @return a compiled {@link Pattern}, or {@code null} if there is an error
* while compiling.
*/
public static Pattern compileRegex(String regex) {
if (android.text.TextUtils.isEmpty(regex))
return null;
try {
return Pattern.compile(regex);
} catch (Throwable t) {
return null;
}
}// compileRegex()
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils;
import group.pals.android.lib.ui.filechooser.BuildConfig;
import android.content.Context;
import android.content.pm.PackageManager;
/**
* Utilities.
*/
public class Utils {
/**
* Checks if the app has <b>all</b> {@code permissions} granted.
*
* @param context
* {@link Context}
* @param permissions
* list of permission names.
* @return {@code true} if the app has all {@code permissions} asked.
*/
public static boolean hasPermissions(Context context, String... permissions) {
for (String p : permissions)
if (context.checkCallingOrSelfPermission(p) == PackageManager.PERMISSION_DENIED)
return false;
return true;
}// hasPermissions()
public static boolean doLog()
{
return false;
//return BuildConfig.DEBUG; //not working with Mono for Android
}
}

View File

@ -0,0 +1,156 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils.history;
import java.util.ArrayList;
import android.os.Parcelable;
/**
* A history store of any object.
*
* @param <A>
* any type
* @author Hai Bison
* @since v2.0 alpha
*/
public interface History<A> extends Parcelable {
/**
* Pushes {@code newItem} to the history. If the top item is same as this
* one, then does nothing.
*
* @param newItem
* the new item
*/
void push(A newItem);
/**
* Finds {@code item} and if it exists, removes all items after it.
*
* @param item
* {@link A}
* @return the total items truncated.
* @since v4.3 beta
*/
int truncateAfter(A item);
/**
* Removes an item.
*
* @param item
* {@link A}
* @since v4.0 beta
*/
void remove(A item);
/**
* Removes all items by a filter.
*
* @param filter
* {@link HistoryFilter}
* @since v4.0 beta
*/
void removeAll(HistoryFilter<A> filter);
/**
* Gets size of the history
*
* @return the size of the history
*/
int size();
/**
* Gets index of item {@code a}
*
* @param a
* an item
* @return index of the {@code a}, or -1 if there is no one
*/
int indexOf(A a);
/**
* Gets previous item of {@code a}
*
* @param a
* current item
* @return the previous item, can be {@code null}
*/
A prevOf(A a);
/**
* Gets next item of {@code a}
*
* @param a
* current item
* @return the next item, can be {@code null}
*/
A nextOf(A a);
/**
* Retrieves all items in this history, in an <i>independent</i> list.
*
* @return list of {@link A}.
* @since v4.3 beta
*/
ArrayList<A> items();
/**
* Checks if the history is empty or not.
*
* @return {@code true} if this history is empty, {@code false} otherwise.
* @since v4.3 beta
*/
boolean isEmpty();
/**
* Clears this history.
*
* @since v4.3 beta.
*/
void clear();
/**
* Adds a {@link HistoryListener}
*
* @param listener
* {@link HistoryListener}
* @since v4.0 beta
*/
void addListener(HistoryListener<A> listener);
/**
* Removes a {@link HistoryListener}
*
* @param listener
* {@link HistoryListener}
* @return the removed listener
* @since v4.0 beta
*/
void removeListener(HistoryListener<A> listener);
/**
* Notifies to all {@link HistoryListener}'s that the history changed.
*/
void notifyHistoryChanged();
/**
* Finds items with a filter.
*
* @param filter
* {@link HistoryFilter}
* @param ascending
* {@code true} if you want to process the history list ascending
* (oldest to newest), {@code false} for descending.
* @return {@code true} if the desired items have been found, {@code false}
* otherwise.
* @since v5.1 beta
*/
boolean find(HistoryFilter<A> filter, boolean ascending);
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils.history;
/**
* Filter of {@link History}
*
* @author Hai Bison
* @since v4.0 beta
*/
public interface HistoryFilter<A> {
/**
* Filters item.
*
* @param item
* {@link A}
* @return {@code true} if the {@code item} is accepted
*/
boolean accept(A item);
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils.history;
/**
* Listener of {@link History}
*
* @author Hai Bison
* @since v4.0 beta
*/
public interface HistoryListener<A> {
/**
* Will be called after the history changed.
*
* @param history
* {@link History}
*/
void onChanged(History<A> history);
}

View File

@ -0,0 +1,263 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils.history;
import java.util.ArrayList;
import java.util.List;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
/**
* A history store of any object extending {@link Parcelable}.
* <p/>
* <b>Note:</b> This class does not support storing its {@link HistoryListener}
* 's into {@link Parcelable}. You must re-build all listeners after getting
* your {@link HistoryStore} from a {@link Bundle} for example.
*
* @author Hai Bison
* @since v2.0 alpha
*/
public class HistoryStore<A extends Parcelable> implements History<A> {
/**
* Uses for debugging...
*/
private static final String CLASSNAME = HistoryStore.class.getName();
/**
* The default capacity of this store.
*/
public static final int DEFAULT_CAPACITY = 99;
private final ArrayList<A> mHistoryList = new ArrayList<A>();
private final List<HistoryListener<A>> mListeners = new ArrayList<HistoryListener<A>>();
private int mCapacity;
/**
* Creates new instance with {@link #DEFAULT_CAPACITY}.
*/
public HistoryStore() {
this(DEFAULT_CAPACITY);
}// HistoryStore()
/**
* Creates new {@link HistoryStore}
*
* @param capcacity
* the maximum size that allowed, if it is {@code <= 0},
* {@link #DEFAULT_CAPACITY} will be used
*/
public HistoryStore(int capcacity) {
mCapacity = capcacity > 0 ? capcacity : DEFAULT_CAPACITY;
}// HistoryStore()
/**
* Gets the capacity.
*
* @return the capacity.
*/
public int getCapacity() {
return mCapacity;
}// getCapacity()
@Override
public void push(A newItem) {
if (newItem == null)
return;
if (!mHistoryList.isEmpty()
&& indexOf(newItem) == mHistoryList.size() - 1)
return;
mHistoryList.add(newItem);
if (mHistoryList.size() > mCapacity)
mHistoryList.remove(0);
notifyHistoryChanged();
}// push()
@Override
public int truncateAfter(A item) {
if (item == null)
return 0;
for (int i = mHistoryList.size() - 2; i >= 0; i--) {
if (mHistoryList.get(i) == item) {
List<A> subList = mHistoryList.subList(i + 1,
mHistoryList.size());
int count = subList.size();
subList.clear();
notifyHistoryChanged();
return count;
}
}
return 0;
}// truncateAfter()
@Override
public void remove(A item) {
if (mHistoryList.remove(item))
notifyHistoryChanged();
}// remove()
@Override
public void removeAll(HistoryFilter<A> filter) {
boolean changed = false;
for (int i = mHistoryList.size() - 1; i >= 0; i--) {
if (filter.accept(mHistoryList.get(i))) {
mHistoryList.remove(i);
if (!changed)
changed = true;
}
}// for
if (changed)
notifyHistoryChanged();
}// removeAll()
@Override
public void notifyHistoryChanged() {
for (HistoryListener<A> listener : mListeners)
listener.onChanged(this);
}// notifyHistoryChanged()
@Override
public int size() {
return mHistoryList.size();
}// size()
@Override
public int indexOf(A a) {
for (int i = 0; i < mHistoryList.size(); i++)
if (mHistoryList.get(i) == a)
return i;
return -1;
}// indexOf()
@Override
public A prevOf(A a) {
int idx = indexOf(a);
if (idx > 0)
return mHistoryList.get(idx - 1);
return null;
}// prevOf()
@Override
public A nextOf(A a) {
int idx = indexOf(a);
if (idx >= 0 && idx < mHistoryList.size() - 1)
return mHistoryList.get(idx + 1);
return null;
}// nextOf()
@SuppressWarnings("unchecked")
@Override
public ArrayList<A> items() {
return (ArrayList<A>) mHistoryList.clone();
}// items()
@Override
public boolean isEmpty() {
return mHistoryList.isEmpty();
}// isEmpty()
@Override
public void clear() {
mHistoryList.clear();
notifyHistoryChanged();
}// clear()
@Override
public void addListener(HistoryListener<A> listener) {
mListeners.add(listener);
}// addListener()
@Override
public void removeListener(HistoryListener<A> listener) {
mListeners.remove(listener);
}// removeListener()
@Override
public boolean find(HistoryFilter<A> filter, boolean ascending) {
for (int i = ascending ? 0 : mHistoryList.size() - 1; ascending ? i < mHistoryList
.size() : i >= 0;) {
if (filter.accept(mHistoryList.get(i)))
return true;
if (ascending)
i++;
else
i--;
}
return false;
}// find()
/*-----------------------------------------------------
* Parcelable
*/
@Override
public int describeContents() {
return 0;
}// describeContents()
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mCapacity);
dest.writeInt(size());
for (int i = 0; i < size(); i++)
dest.writeParcelable(mHistoryList.get(i), flags);
}// writeToParcel()
/**
* Reads data from {@code in}.
*
* @param in
* {@link Parcel}.
*/
@SuppressWarnings("unchecked")
public void readFromParcel(Parcel in) {
mCapacity = in.readInt();
int count = in.readInt();
for (int i = 0; i < count; i++) {
try {
mHistoryList.add((A) in.readParcelable(getClass()
.getClassLoader()));
} catch (ClassCastException e) {
Log.e(CLASSNAME, "readFromParcel() >> " + e);
e.printStackTrace();
break;
}
}
}// readFromParcel()
public static final Parcelable.Creator<HistoryStore<?>> CREATOR = new Parcelable.Creator<HistoryStore<?>>() {
@SuppressWarnings("rawtypes")
public HistoryStore<?> createFromParcel(Parcel in) {
return new HistoryStore(in);
}// createFromParcel()
public HistoryStore<?>[] newArray(int size) {
return new HistoryStore[size];
}// newArray()
};// CREATOR
private HistoryStore(Parcel in) {
readFromParcel(in);
}// HistoryStore()
}

View File

@ -0,0 +1,133 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils.ui;
import group.pals.android.lib.ui.filechooser.R;
import android.app.Dialog;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ListView;
/**
* Utilities for context menu.
*
* @author Hai Bison
* @since v4.3 beta
*/
public class ContextMenuUtils {
/**
* Shows context menu.
*
* @param context
* {@link Context}
* @param iconId
* resource icon ID of the dialog.
* @param title
* title of the dialog.
* @param itemIds
* array of resource IDs of strings.
* @param listener
* {@link OnMenuItemClickListener}
*/
public static void showContextMenu(Context context, int iconId,
String title, final Integer[] itemIds,
final OnMenuItemClickListener listener) {
final Dialog dialog = new Dialog(context, Ui.resolveAttribute(context,
R.attr.afc_theme_dialog));
dialog.setCanceledOnTouchOutside(true);
if (iconId > 0)
dialog.requestWindowFeature(Window.FEATURE_LEFT_ICON);
if (TextUtils.isEmpty(title))
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
else
dialog.setTitle(title);
final MenuItemAdapter _adapter = new MenuItemAdapter(
dialog.getContext(), itemIds);
View view = LayoutInflater.from(context).inflate(
R.layout.afc_context_menu_view, null);
ListView listview = (ListView) view
.findViewById(R.id.afc_listview_menu);
listview.setAdapter(_adapter);
dialog.setContentView(view);
if (iconId > 0)
dialog.setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, iconId);
if (listener != null) {
listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
dialog.dismiss();
listener.onClick(itemIds[position]);
}// onItemClick()
});
}// if listener != null
dialog.show();
/*
* Hardcode width...
*/
WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
lp.copyFrom(dialog.getWindow().getAttributes());
lp.width = context.getResources().getDimensionPixelSize(
R.dimen.afc_context_menu_width);
dialog.getWindow().setAttributes(lp);
}// showContextMenu()
/**
* Shows context menu.
*
* @param context
* {@link Context}
* @param iconId
* resource icon ID of the dialog.
* @param titleId
* resource ID of the title of the dialog. {@code 0} will be
* ignored.
* @param itemIds
* array of resource IDs of strings.
* @param listener
* {@link OnMenuItemClickListener}
*/
public static void showContextMenu(Context context, int iconId,
int titleId, Integer[] itemIds, OnMenuItemClickListener listener) {
showContextMenu(context, iconId,
titleId > 0 ? context.getString(titleId) : null, itemIds,
listener);
}// showContextMenu()
// ==========
// INTERFACES
/**
* @author Hai Bison
* @since v4.3 beta
*/
public static interface OnMenuItemClickListener {
/**
* This method will be called after the menu dismissed.
*
* @param resId
* the resource ID of the title of the menu item.
*/
void onClick(int resId);
}// OnMenuItemClickListener
}

View File

@ -0,0 +1,267 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils.ui;
import group.pals.android.lib.ui.filechooser.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.view.ContextThemeWrapper;
import android.widget.Toast;
/**
* Utilities for message boxes.
*
* @author Hai Bison
* @since v2.1 alpha
*/
public class Dlg {
/**
* @see Toast#LENGTH_SHORT
*/
public static final int LENGTH_SHORT = android.widget.Toast.LENGTH_SHORT;
/**
* @see Toast#LENGTH_LONG
*/
public static final int LENGTH_LONG = android.widget.Toast.LENGTH_LONG;
private static android.widget.Toast mToast;
/**
* Shows a toast message.
*
* @param context
* {@link Context}
* @param msg
* the message.
* @param duration
* can be {@link #LENGTH_LONG} or {@link #LENGTH_SHORT}.
*/
public static void toast(Context context, CharSequence msg, int duration) {
if (mToast != null)
mToast.cancel();
mToast = android.widget.Toast.makeText(context, msg, duration);
mToast.show();
}// mToast()
/**
* Shows a toast message.
*
* @param context
* {@link Context}
* @param msgId
* the resource ID of the message.
* @param duration
* can be {@link #LENGTH_LONG} or {@link #LENGTH_SHORT}.
*/
public static void toast(Context context, int msgId, int duration) {
toast(context, context.getString(msgId), duration);
}// mToast()
/**
* Shows an info dialog.
*
* @param context
* {@link Context}
* @param msg
* the message.
* @param listener
* the {@link DialogInterface.OnDismissListener}.
*/
public static void showInfo(Context context, CharSequence msg,
DialogInterface.OnDismissListener listener) {
AlertDialog dlg = newAlertDlg(context);
dlg.setIcon(android.R.drawable.ic_dialog_info);
dlg.setTitle(R.string.afc_title_info);
dlg.setMessage(msg);
dlg.setOnDismissListener(listener);
dlg.show();
}// showInfo()
/**
* Shows an info dialog.
*
* @param context
* the context.
* @param msgId
* the resource ID of the message.
* @param listener
* the {@link DialogInterface.OnDismissListener}.
*/
public static void showInfo(Context context, int msgId,
DialogInterface.OnDismissListener listener) {
showInfo(context, context.getString(msgId), listener);
}// showInfo()
/**
* Shows an info dialog.
*
* @param context
* {@link Context}
* @param msg
* the message.
*/
public static void showInfo(Context context, CharSequence msg) {
showInfo(context, msg, null);
}// showInfo()
/**
* Shows an info dialog.
*
* @param context
* {@link Context}
* @param msgId
* the resource ID of the message.
*/
public static void showInfo(Context context, int msgId) {
showInfo(context, context.getString(msgId));
}// showInfo()
/**
* Shows an error message.
*
* @param context
* {@link Context}
* @param msg
* the message.
* @param listener
* will be called after the user cancelled the dialog.
*/
public static void showError(Context context, CharSequence msg,
DialogInterface.OnCancelListener listener) {
AlertDialog dlg = newAlertDlg(context);
dlg.setIcon(android.R.drawable.ic_dialog_alert);
dlg.setTitle(R.string.afc_title_error);
dlg.setMessage(msg);
dlg.setOnCancelListener(listener);
dlg.show();
}// showError()
/**
* Shows an error message.
*
* @param context
* {@link Context}
* @param msgId
* the resource ID of the message.
* @param listener
* will be called after the user cancelled the dialog.
*/
public static void showError(Context context, int msgId,
DialogInterface.OnCancelListener listener) {
showError(context, context.getString(msgId), listener);
}// showError()
/**
* Shows an unknown error.
*
* @param context
* {@link Context}
* @param t
* the {@link Throwable}
* @param listener
* will be called after the user cancelled the dialog.
*/
public static void showUnknownError(Context context, Throwable t,
DialogInterface.OnCancelListener listener) {
showError(
context,
String.format(
context.getString(R.string.afc_pmsg_unknown_error), t),
listener);
}// showUnknownError()
/**
* Shows a confirmation dialog.
*
* @param context
* {@link Context}
* @param msg
* the message.
* @param onYes
* will be called if the user selects positive answer (a
* <i>Yes</i> or <i>OK</i>).
* @param onNo
* will be called after the user cancelled the dialog.
*/
public static void confirmYesno(Context context, CharSequence msg,
DialogInterface.OnClickListener onYes,
DialogInterface.OnCancelListener onNo) {
AlertDialog dlg = newAlertDlg(context);
dlg.setIcon(android.R.drawable.ic_dialog_alert);
dlg.setTitle(R.string.afc_title_confirmation);
dlg.setMessage(msg);
dlg.setButton(DialogInterface.BUTTON_POSITIVE,
context.getString(android.R.string.yes), onYes);
dlg.setOnCancelListener(onNo);
dlg.show();
}
/**
* Shows a confirmation dialog.
*
* @param context
* {@link Context}
* @param msg
* the message.
* @param onYes
* will be called if the user selects positive answer (a
* <i>Yes</i> or <i>OK</i>).
*/
public static void confirmYesno(Context context, CharSequence msg,
DialogInterface.OnClickListener onYes) {
confirmYesno(context, msg, onYes, null);
}// confirmYesno()
/**
* Creates new {@link Dialog}. Set canceled on touch outside to {@code true}
* .
*
* @param context
* the context which uses this library's theme.
* @return the {@link Dialog}.
* @since v4.3 beta
*/
public static Dialog newDlg(Context context) {
Dialog res = new Dialog(context, Ui.resolveAttribute(context,
R.attr.afc_theme_dialog));
res.setCanceledOnTouchOutside(true);
return res;
}// newAlertDlg()
/**
* Creates new {@link AlertDialog}. Set canceled on touch outside to
* {@code true}.
*
* @param context
* the context which uses this library's theme.
* @return {@link AlertDialog}
* @since v4.3 beta
*/
public static AlertDialog newAlertDlg(Context context) {
AlertDialog res = newAlertDlgBuilder(context).create();
res.setCanceledOnTouchOutside(true);
return res;
}// newAlertDlg()
/**
* Creates new {@link AlertDialog.Builder}.
*
* @param context
* the context which uses this library's theme.
* @return {@link AlertDialog}
* @since v4.3 beta
*/
public static AlertDialog.Builder newAlertDlgBuilder(Context context) {
return new AlertDialog.Builder(new ContextThemeWrapper(context,
Ui.resolveAttribute(context, R.attr.afc_theme_dialog)));
}// newAlertDlgBuilder()
}

View File

@ -0,0 +1,222 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils.ui;
import group.pals.android.lib.ui.filechooser.BuildConfig;
import android.graphics.Rect;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AbsListView;
/**
* Utilities for user's gesture.
*
* @author Hai Bison
* @since v5.1 beta
*/
public class GestureUtils {
private static final String CLASSNAME = GestureUtils.class.getName();
/**
* The fling direction.
*
* @author Hai Bison
* @since v5.1 beta
*/
public static enum FlingDirection {
LEFT_TO_RIGHT, RIGHT_TO_LEFT, UNKNOWN
}// FlingDirection
/**
* Calculates fling direction from two {@link MotionEvent} and their
* velocity.
*
* @param e1
* {@link MotionEvent}
* @param e2
* {@link MotionEvent}
* @param velocityX
* the X velocity.
* @param velocityY
* the Y velocity.
* @return {@link FlingDirection}
*/
public static FlingDirection calcFlingDirection(MotionEvent e1,
MotionEvent e2, float velocityX, float velocityY) {
if (e1 == null || e2 == null)
return FlingDirection.UNKNOWN;
final int _max_y_distance = 19;// 10 is too short :-D
final int _min_x_distance = 80;
final int _min_x_velocity = 200;
if (Math.abs(e1.getY() - e2.getY()) < _max_y_distance
&& Math.abs(e1.getX() - e2.getX()) > _min_x_distance
&& Math.abs(velocityX) > _min_x_velocity) {
return velocityX <= 0 ? FlingDirection.LEFT_TO_RIGHT
: FlingDirection.RIGHT_TO_LEFT;
}
return FlingDirection.UNKNOWN;
}// calcFlingDirection()
/**
* Interface for user's gesture.
*
* @author Hai Bison
* @since v5.1 beta
*/
public static interface OnGestureListener {
/**
* Will be called after the user did a single tap.
*
* @param view
* the selected view.
* @param data
* the data.
* @return {@code true} if you want to handle the event, otherwise
* {@code false}.
*/
boolean onSingleTapConfirmed(View view, Object data);
/**
* Will be notified after the user flung the view.
*
* @param view
* the selected view.
* @param data
* the data.
* @param flingDirection
* {@link FlingDirection}.
* @return {@code true} if you handled this event, {@code false} if you
* want to let default handler handle it.
*/
boolean onFling(View view, Object data, FlingDirection flingDirection);
}// OnGestureListener
/**
* An adapter of {@link OnGestureListener}.
*
* @author Hai Bison
* @since v5.1 beta
*/
public static class SimpleOnGestureListener implements OnGestureListener {
@Override
public boolean onSingleTapConfirmed(View view, Object data) {
return false;
}
@Override
public boolean onFling(View view, Object data,
FlingDirection flingDirection) {
return false;
}
}// SimpleOnGestureListener
/**
* Adds a gesture listener to {@code listView}.
*
* @param listView
* {@link AbsListView}.
* @param listener
* {@link OnGestureListener}.
*/
public static void setupGestureDetector(final AbsListView listView,
final OnGestureListener listener) {
final GestureDetector _gestureDetector = new GestureDetector(
listView.getContext(),
new GestureDetector.SimpleOnGestureListener() {
private Object getData(float x, float y) {
int i = getSubViewId(x, y);
if (i >= 0)
return listView.getItemAtPosition(listView
.getFirstVisiblePosition() + i);
return null;
}// getSubView()
private View getSubView(float x, float y) {
int i = getSubViewId(x, y);
if (i >= 0)
return listView.getChildAt(i);
return null;
}// getSubView()
private int getSubViewId(float x, float y) {
Rect r = new Rect();
for (int i = 0; i < listView.getChildCount(); i++) {
listView.getChildAt(i).getHitRect(r);
if (r.contains((int) x, (int) y)) {
if (BuildConfig.DEBUG)
Log.d(CLASSNAME,
String.format(
"getSubViewId() -- left-top-right-bottom = %d-%d-%d-%d",
r.left, r.top, r.right,
r.bottom));
return i;
}
}
return -1;
}// getSubViewId()
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (BuildConfig.DEBUG)
Log.d(CLASSNAME,
String.format(
"onSingleTapConfirmed() -- x = %.2f -- y = %.2f",
e.getX(), e.getY()));
return listener == null ? false : listener
.onSingleTapConfirmed(
getSubView(e.getX(), e.getY()),
getData(e.getX(), e.getY()));
}// onSingleTapConfirmed()
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
if (listener == null || e1 == null || e2 == null)
return false;
FlingDirection fd = calcFlingDirection(e1, e2,
velocityX, velocityY);
if (!FlingDirection.UNKNOWN.equals(fd)) {
if (listener.onFling(
getSubView(e1.getX(), e1.getY()),
getData(e1.getX(), e1.getY()), fd)) {
MotionEvent cancelEvent = MotionEvent
.obtain(e1);
cancelEvent
.setAction(MotionEvent.ACTION_CANCEL);
listView.onTouchEvent(cancelEvent);
}
}
/*
* Always return false to let the default handler draw
* the item properly.
*/
return false;
}// onFling()
});// _gestureDetector
listView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return _gestureDetector.onTouchEvent(event);
}
});
}// setupGestureDetector()
}

View File

@ -0,0 +1,198 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils.ui;
import group.pals.android.lib.ui.filechooser.R;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Handler;
import android.util.Log;
/**
* An {@link AsyncTask}, used to show {@link ProgressDialog} while doing some
* background tasks.
*
* @author Hai Bison
* @since v2.1 alpha
*/
public abstract class LoadingDialog<Params, Progress, Result> extends
AsyncTask<Params, Progress, Result> {
private static final String CLASSNAME = LoadingDialog.class.getName();
private final ProgressDialog mDialog;
/**
* Default is {@code 500}ms
*/
private int mDelayTime = 500;
/**
* Flag to use along with {@link #mDelayTime}
*/
private boolean mFinished = false;
private Throwable mLastException;
/**
* Creates new {@link LoadingDialog}
*
* @param context
* {@link Context}
* @param msg
* message will be shown in the dialog.
* @param cancelable
* as the name means.
*/
public LoadingDialog(Context context, String msg, boolean cancelable) {
mDialog = new ProgressDialog(context);
mDialog.setMessage(msg);
mDialog.setIndeterminate(true);
mDialog.setCancelable(cancelable);
if (cancelable) {
mDialog.setCanceledOnTouchOutside(true);
mDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
cancel(true);
}
});
}
}// LoadingDialog()
/**
* Creates new {@link LoadingDialog}
*
* @param context
* {@link Context}
* @param msgId
* resource id of the message will be shown in the dialog.
* @param cancelable
* as the name means.
*/
public LoadingDialog(Context context, int msgId, boolean cancelable) {
this(context, context.getString(msgId), cancelable);
}// LoadingDialog()
/**
* Creates new {@link LoadingDialog} showing "Loading..." (
* {@link R.string#afc_msg_loading}).
*
* @param context
* {@link Context}
* @param cancelable
* as the name means.
*/
public LoadingDialog(Context context, boolean cancelable) {
this(context, context.getString(R.string.afc_msg_loading), cancelable);
}// LoadingDialog()
/**
* If you override this method, you must call {@code super.onPreExecute()}
* at beginning of the method.
*/
@Override
protected void onPreExecute() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (!mFinished) {
try {
/*
* sometime the activity has been finished before we
* show this dialog, it will raise error
*/
mDialog.show();
} catch (Throwable t) {
// TODO
Log.e(CLASSNAME, "onPreExecute() - show dialog: " + t);
}
}
}
}, getDelayTime());
}// onPreExecute()
/**
* If you override this method, you must call
* {@code super.onPostExecute(result)} at beginning of the method.
*/
@Override
protected void onPostExecute(Result result) {
doFinish();
}// onPostExecute()
/**
* If you override this method, you must call {@code super.onCancelled()} at
* beginning of the method.
*/
@Override
protected void onCancelled() {
doFinish();
super.onCancelled();
}// onCancelled()
private void doFinish() {
mFinished = true;
try {
/*
* Sometime the activity has been finished before we dismiss this
* dialog, it will raise error.
*/
mDialog.dismiss();
} catch (Throwable t) {
// TODO
Log.e(CLASSNAME, "doFinish() - dismiss dialog: " + t);
}
}// doFinish()
/**
* Gets the delay time before showing the dialog.
*
* @return the delay time
*/
public int getDelayTime() {
return mDelayTime;
}// getDelayTime()
/**
* Sets the delay time before showing the dialog.
*
* @param delayTime
* the delay time to set
* @return the instance of this dialog, for chaining multiple calls into a
* single statement.
*/
public LoadingDialog<Params, Progress, Result> setDelayTime(int delayTime) {
mDelayTime = delayTime >= 0 ? delayTime : 0;
return this;
}// setDelayTime()
/**
* Sets last exception. This method is useful in case an exception raises
* inside {@link #doInBackground(Void...)}
*
* @param t
* {@link Throwable}
*/
protected void setLastException(Throwable t) {
mLastException = t;
}// setLastException()
/**
* Gets last exception.
*
* @return {@link Throwable}
*/
protected Throwable getLastException() {
return mLastException;
}// getLastException()
}

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils.ui;
import group.pals.android.lib.ui.filechooser.R;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
/**
* Adapter for context menu.
*
* @author Hai Bison
* @since v4.3 beta
*/
public class MenuItemAdapter extends BaseAdapter {
private final Context mContext;
private final Integer[] mItems;
/**
* Creates new instance.
*
* @param context
* {@link Context}
* @param itemIds
* array of resource IDs of titles to be used.
*/
public MenuItemAdapter(Context context, Integer[] itemIds) {
mContext = context;
mItems = itemIds;
}// MenuItemAdapter()
@Override
public int getCount() {
return mItems.length;
}// getCount()
@Override
public Object getItem(int position) {
return mItems[position];
}// getItem()
@Override
public long getItemId(int position) {
return position;
}// getItemId()
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(
R.layout.afc_context_menu_tiem, null);
}
((TextView) convertView).setText(mItems[position]);
return convertView;
}// getView()
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils.ui;
/**
* The listener for any task you want to assign to.
*
* @author Hai Bison
* @since v1.8
*/
public interface TaskListener {
/**
* Will be called after the task finished.
*
* @param ok
* {@code true} if everything is OK, {@code false} otherwise.
* @param any
* the user data, can be {@code null}.
*/
public void onFinish(boolean ok, Object any);
}

View File

@ -0,0 +1,149 @@
/*
* Copyright (c) 2012 Hai Bison
*
* See the file LICENSE at the root directory of this project for copying
* permission.
*/
package group.pals.android.lib.ui.filechooser.utils.ui;
import group.pals.android.lib.ui.filechooser.BuildConfig;
import group.pals.android.lib.ui.filechooser.R;
import android.app.Dialog;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Paint;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.Window;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
/**
* UI utilities.
*
* @author Hai Bison
*/
public class Ui {
private static final String CLASSNAME = Ui.class.getName();
/**
* Shows/ hides soft input (soft keyboard).
*
* @param view
* {@link View}.
* @param show
* {@code true} or {@code false}. If {@code true}, this method
* will use a {@link Runnable} to show the IMM. So you don't need
* to use it, and consider using
* {@link View#removeCallbacks(Runnable)} if you want to cancel.
*/
public static void showSoftKeyboard(final View view, final boolean show) {
final InputMethodManager imm = (InputMethodManager) view.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm == null)
return;
if (show) {
view.post(new Runnable() {
@Override
public void run() {
imm.showSoftInput(view, 0, null);
}// run()
});
} else
imm.hideSoftInputFromWindow(view.getWindowToken(), 0, null);
}// showSoftKeyboard()
/**
* Strikes out text of {@code view}.
*
* @param view
* {@link TextView}.
* @param strikeOut
* {@code true} to strike out the text.
*/
public static void strikeOutText(TextView view, boolean strikeOut) {
if (strikeOut)
view.setPaintFlags(view.getPaintFlags()
| Paint.STRIKE_THRU_TEXT_FLAG);
else
view.setPaintFlags(view.getPaintFlags()
& ~Paint.STRIKE_THRU_TEXT_FLAG);
}// strikeOutText()
/**
* Convenient method for {@link Context#getTheme()} and
* {@link Resources.Theme#resolveAttribute(int, TypedValue, boolean)}.
*
* @param context
* the context.
* @param resId
* The resource identifier of the desired theme attribute.
* @return the resource ID that {@link TypedValue#resourceId} points to, or
* {@code 0} if not found.
*/
public static int resolveAttribute(Context context, int resId) {
TypedValue typedValue = new TypedValue();
if (context.getTheme().resolveAttribute(resId, typedValue, true))
return typedValue.resourceId;
return 0;
}// resolveAttribute()
/**
* Uses a fixed size for {@code dialog} in large screens.
*
* @param dialog
* the dialog.
*/
public static void adjustDialogSizeForLargeScreen(Dialog dialog) {
adjustDialogSizeForLargeScreen(dialog.getWindow());
}// adjustDialogSizeForLargeScreen()
/**
* Uses a fixed size for {@code window} in large screens.
*
* @param dialogWindow
* the window <i>of the dialog</i>.
*/
public static void adjustDialogSizeForLargeScreen(Window dialogWindow) {
if (BuildConfig.DEBUG)
Log.d(CLASSNAME, "adjustDialogSizeForLargeScreen()");
if (dialogWindow.isFloating()
&& dialogWindow.getContext().getResources()
.getBoolean(R.bool.afc_is_large_screen)) {
final DisplayMetrics metrics = dialogWindow.getContext()
.getResources().getDisplayMetrics();
final boolean isPortrait = metrics.widthPixels < metrics.heightPixels;
int width = metrics.widthPixels;// dialogWindow.getDecorView().getWidth();
int height = metrics.heightPixels;// dialogWindow.getDecorView().getHeight();
if (BuildConfig.DEBUG)
Log.d(CLASSNAME, String.format("width = %,d | height = %,d",
width, height));
width = (int) dialogWindow
.getContext()
.getResources()
.getFraction(
isPortrait ? R.dimen.aosp_dialog_fixed_width_minor
: R.dimen.aosp_dialog_fixed_width_major,
width, width);
height = (int) dialogWindow
.getContext()
.getResources()
.getFraction(
isPortrait ? R.dimen.aosp_dialog_fixed_height_major
: R.dimen.aosp_dialog_fixed_height_minor,
height, height);
if (BuildConfig.DEBUG)
Log.d(CLASSNAME, String.format(
"NEW >>> width = %,d | height = %,d", width, height));
dialogWindow.setLayout(width, height);
}
}// adjustDialogSizeForLargeScreen()
}

View File

@ -0,0 +1,18 @@
package keepass2android.kp2afilechooser;
public class FileEntry {
public String path;
public String displayName;
public boolean isDirectory;
public long lastModifiedTime;
public boolean canRead;
public boolean canWrite;
public long sizeInBytes;
public FileEntry()
{
isDirectory = false;
canRead = canWrite = true;
}
}

View File

@ -0,0 +1,24 @@
package keepass2android.kp2afilechooser;
import group.pals.android.lib.ui.filechooser.FileChooserActivity;
//import group.pals.android.lib.ui.filechooser.FileChooserActivity_v7;
import group.pals.android.lib.ui.filechooser.providers.basefile.BaseFileContract.BaseFile;
import android.content.Context;
import android.content.Intent;
public class Kp2aFileChooserBridge {
public static Intent getLaunchFileChooserIntent(Context ctx, String authority, String defaultPath)
{
//Always use FileChooserActivity. _v7 was removed due to problems with Mono for Android binding.
Class<?> cls = FileChooserActivity.class;
Intent intent = new Intent(ctx, cls);
intent.putExtra(FileChooserActivity.EXTRA_FILE_PROVIDER_AUTHORITY, authority);
intent.putExtra(FileChooserActivity.EXTRA_ROOTPATH,
BaseFile.genContentIdUriBase(authority)
.buildUpon()
.appendPath(defaultPath)
.build());
return intent;
}
}

View File

@ -0,0 +1,770 @@
package keepass2android.kp2afilechooser;
/* Author: Philipp Crocoll
*
* Based on a file provider by Hai Bison
*
*/
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CancellationException;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.net.Uri;
import android.util.Log;
import group.pals.android.lib.ui.filechooser.R;
import group.pals.android.lib.ui.filechooser.providers.BaseFileProviderUtils;
import group.pals.android.lib.ui.filechooser.providers.ProviderUtils;
import group.pals.android.lib.ui.filechooser.providers.basefile.BaseFileContract.BaseFile;
import group.pals.android.lib.ui.filechooser.providers.basefile.BaseFileProvider;
import group.pals.android.lib.ui.filechooser.utils.FileUtils;
import group.pals.android.lib.ui.filechooser.utils.Utils;
public abstract class Kp2aFileProvider extends BaseFileProvider {
/**
* Gets the authority of this provider.
*
* abstract because the concrete authority can be decided by the overriding class.
*
* @param context the context.
* @return the authority.
*/
public abstract String getAuthority();
/**
* The unique ID of this provider.
*/
public static final String _ID = "9dab9818-0a8b-47ef-88cc-10fe538bf8f7";
/**
* Used for debugging or something...
*/
private static final String CLASSNAME = Kp2aFileProvider.class.getName();
//cache for FileEntry objects to reduce network traffic
private HashMap<String, FileEntry> fileEntryMap = new HashMap<String, FileEntry>();
//during write operations it is not desired to put entries to the cache. This set indicates which
//files cannot be cached currently:
private Set<String> cacheBlockedFiles = new HashSet<String>();
@Override
public boolean onCreate() {
BaseFileProviderUtils.registerProviderInfo(_ID,
getAuthority());
URI_MATCHER.addURI(getAuthority(),
BaseFile.PATH_DIR + "/*", URI_DIRECTORY);
URI_MATCHER.addURI(getAuthority(),
BaseFile.PATH_FILE + "/*", URI_FILE);
URI_MATCHER.addURI(getAuthority(),
BaseFile.PATH_API, URI_API);
URI_MATCHER.addURI(getAuthority(),
BaseFile.PATH_API + "/*", URI_API_COMMAND);
return true;
}// onCreate()
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
if (Utils.doLog())
Log.d(CLASSNAME, "delete() >> " + uri);
int count = 0;
switch (URI_MATCHER.match(uri)) {
case URI_FILE: {
boolean isRecursive = ProviderUtils.getBooleanQueryParam(uri,
BaseFile.PARAM_RECURSIVE, true);
String filename = extractFile(uri);
removeFromCache(filename, isRecursive);
blockFromCache(filename);
if (deletePath(filename, isRecursive))
{
getContext()
.getContentResolver()
.notifyChange(
BaseFile.genContentUriBase(
getAuthority())
.buildUpon()
.appendPath(
getParentPath(filename)
)
.build(), null);
count = 1; //success
}
blockFromCache(filename);
break;// URI_FILE
}
default:
throw new IllegalArgumentException("UNKNOWN URI " + uri);
}
if (count > 0)
getContext().getContentResolver().notifyChange(uri, null);
return count;
}// delete()
@Override
public Uri insert(Uri uri, ContentValues values) {
if (Utils.doLog())
Log.d(CLASSNAME, "insert() >> " + uri);
switch (URI_MATCHER.match(uri)) {
case URI_DIRECTORY:
String dirname = extractFile(uri);
String newDirName = uri.getQueryParameter(BaseFile.PARAM_NAME);
String newFullName = removeTrailingSlash(dirname)+"/"+newDirName;
boolean success = false;
switch (ProviderUtils.getIntQueryParam(uri,
BaseFile.PARAM_FILE_TYPE, BaseFile.FILE_TYPE_DIRECTORY)) {
case BaseFile.FILE_TYPE_DIRECTORY:
success = createDirectory(dirname, newDirName);
break;// FILE_TYPE_DIRECTORY
case BaseFile.FILE_TYPE_FILE:
//not supported at the moment
break;// FILE_TYPE_FILE
default:
return null;
}
if (success)
{
Uri newUri = BaseFile
.genContentIdUriBase(
getAuthority())
.buildUpon()
.appendPath( newFullName).build();
getContext().getContentResolver().notifyChange(uri, null);
return newUri;
}
return null;// URI_FILE
default:
throw new IllegalArgumentException("UNKNOWN URI " + uri);
}
}// insert()
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
if (Utils.doLog())
Log.d(CLASSNAME, String.format(
"query() >> uri = %s (%s) >> match = %s", uri,
uri.getLastPathSegment(), URI_MATCHER.match(uri)));
switch (URI_MATCHER.match(uri)) {
case URI_API: {
/*
* If there is no command given, return provider ID and name.
*/
MatrixCursor matrixCursor = new MatrixCursor(new String[] {
BaseFile.COLUMN_PROVIDER_ID, BaseFile.COLUMN_PROVIDER_NAME,
BaseFile.COLUMN_PROVIDER_ICON_ATTR });
matrixCursor.newRow().add(_ID)
.add("KP2A")
.add(R.attr.afc_badge_file_provider_localfile);
return matrixCursor;
}
case URI_API_COMMAND: {
return doAnswerApiCommand(uri);
}// URI_API
case URI_DIRECTORY: {
return doListFiles(uri);
}// URI_DIRECTORY
case URI_FILE: {
return doRetrieveFileInfo(uri);
}// URI_FILE
default:
throw new IllegalArgumentException("UNKNOWN URI " + uri);
}
}// query()
/*
* UTILITIES
*/
/**
* Answers the incoming URI.
*
* @param uri
* the request URI.
* @return the response.
*/
private MatrixCursor doAnswerApiCommand(Uri uri) {
MatrixCursor matrixCursor = null;
String lastPathSegment = uri.getLastPathSegment();
//Log.d(CLASSNAME, "lastPathSegment:" + lastPathSegment);
if (BaseFile.CMD_CANCEL.equals(lastPathSegment)) {
int taskId = ProviderUtils.getIntQueryParam(uri,
BaseFile.PARAM_TASK_ID, 0);
synchronized (mMapInterruption) {
if (taskId == 0) {
for (int i = 0; i < mMapInterruption.size(); i++)
mMapInterruption.put(mMapInterruption.keyAt(i), true);
} else if (mMapInterruption.indexOfKey(taskId) >= 0)
mMapInterruption.put(taskId, true);
}
return null;
} else if (BaseFile.CMD_GET_DEFAULT_PATH.equals(lastPathSegment)) {
return null;
}// get default path
else if (BaseFile.CMD_IS_ANCESTOR_OF.equals(lastPathSegment)) {
return doCheckAncestor(uri);
} else if (BaseFile.CMD_GET_PARENT.equals(lastPathSegment)) {
{
String path = Uri.parse(
uri.getQueryParameter(BaseFile.PARAM_SOURCE)).toString();
String parentPath = getParentPath(path);
if (parentPath == null)
{
if (Utils.doLog())
Log.d(CLASSNAME, "parent file is null");
return null;
}
FileEntry e = this.getFileEntryCached(parentPath);
matrixCursor = BaseFileProviderUtils.newBaseFileCursor();
int type = parentPath != null ? BaseFile.FILE_TYPE_DIRECTORY
: BaseFile.FILE_TYPE_NOT_EXISTED;
RowBuilder newRow = matrixCursor.newRow();
newRow.add(0);// _ID
newRow.add(BaseFile
.genContentIdUriBase(
getAuthority())
.buildUpon().appendPath(parentPath)
.build().toString());
newRow.add(e.path);
newRow.add(e.displayName);
newRow.add(e.canRead); //can read
newRow.add(e.canWrite); //can write
newRow.add(0);
newRow.add(type);
newRow.add(0);
newRow.add(FileUtils.getResIcon(type, e.displayName));
return matrixCursor;
}
} else if (BaseFile.CMD_SHUTDOWN.equals(lastPathSegment)) {
/*
* TODO Stop all tasks. If the activity call this command in
* onDestroy(), it seems that this code block will be suspended and
* started next time the activity starts. So we comment out this.
* Let the Android system do what it wants to do!!!! I hate this.
*/
// synchronized (mMapInterruption) {
// for (int i = 0; i < mMapInterruption.size(); i++)
// mMapInterruption.put(mMapInterruption.keyAt(i), true);
// }
}
return matrixCursor;
}// doAnswerApiCommand()
/*
private String addProtocol(String path) {
if (path == null)
return null;
if (path.startsWith(getProtocolId()+"://"))
return path;
return getProtocolId()+"://"+path;
}*/
/**
* Lists the content of a directory, if available.
*
* @param uri
* the URI pointing to a directory.
* @return the content of a directory, or {@code null} if not available.
*/
private MatrixCursor doListFiles(Uri uri) {
MatrixCursor matrixCursor = BaseFileProviderUtils.newBaseFileCursor();
String dirName = extractFile(uri);
if (Utils.doLog())
Log.d(CLASSNAME, "doListFiles. srcFile = " + dirName);
/*
* Prepare params...
*/
int taskId = ProviderUtils.getIntQueryParam(uri,
BaseFile.PARAM_TASK_ID, 0);
boolean showHiddenFiles = ProviderUtils.getBooleanQueryParam(uri,
BaseFile.PARAM_SHOW_HIDDEN_FILES);
boolean sortAscending = ProviderUtils.getBooleanQueryParam(uri,
BaseFile.PARAM_SORT_ASCENDING, true);
int sortBy = ProviderUtils.getIntQueryParam(uri,
BaseFile.PARAM_SORT_BY, BaseFile.SORT_BY_NAME);
int filterMode = ProviderUtils.getIntQueryParam(uri,
BaseFile.PARAM_FILTER_MODE,
BaseFile.FILTER_FILES_AND_DIRECTORIES);
int limit = ProviderUtils.getIntQueryParam(uri, BaseFile.PARAM_LIMIT,
1000);
String positiveRegex = uri
.getQueryParameter(BaseFile.PARAM_POSITIVE_REGEX_FILTER);
String negativeRegex = uri
.getQueryParameter(BaseFile.PARAM_NEGATIVE_REGEX_FILTER);
mMapInterruption.put(taskId, false);
boolean[] hasMoreFiles = { false };
List<FileEntry> files = new ArrayList<FileEntry>();
listFiles(taskId, dirName, showHiddenFiles, filterMode, limit,
positiveRegex, negativeRegex, files, hasMoreFiles);
if (!mMapInterruption.get(taskId)) {
try {
sortFiles(taskId, files, sortAscending, sortBy);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (!mMapInterruption.get(taskId)) {
for (int i = 0; i < files.size(); i++) {
if (mMapInterruption.get(taskId))
break;
FileEntry f = files.get(i);
updateFileEntryCache(f);
if (Utils.doLog())
Log.d(CLASSNAME, "listing " + f.path +" for "+dirName);
addFileInfo(matrixCursor, i, f);
}// for files
/*
* The last row contains:
*
* - The ID;
*
* - The base file URI to original directory, which has
* parameter BaseFile.PARAM_HAS_MORE_FILES to indicate the
* directory has more files or not.
*
* - The system absolute path to original directory.
*
* - The name of original directory.
*/
RowBuilder newRow = matrixCursor.newRow();
newRow.add(files.size());// _ID
newRow.add(BaseFile
.genContentIdUriBase(
getAuthority())
.buildUpon()
.appendPath(dirName)
.appendQueryParameter(BaseFile.PARAM_HAS_MORE_FILES,
Boolean.toString(hasMoreFiles[0])).build()
.toString());
newRow.add(dirName);
String displayName = getFileEntryCached(dirName).displayName;
newRow.add(displayName);
Log.d(CLASSNAME, "Returning name " + displayName+" for " +dirName);
}
}
try {
if (mMapInterruption.get(taskId)) {
if (Utils.doLog())
Log.d(CLASSNAME, "query() >> cancelled...");
return null;
}
} finally {
mMapInterruption.delete(taskId);
}
/*
* Tells the Cursor what URI to watch, so it knows when its source data
* changes.
*/
matrixCursor.setNotificationUri(getContext().getContentResolver(), uri);
return matrixCursor;
}// doListFiles()
private RowBuilder addFileInfo(MatrixCursor matrixCursor, int id,
FileEntry f) {
int type = !f.isDirectory ? BaseFile.FILE_TYPE_FILE : BaseFile.FILE_TYPE_DIRECTORY;
RowBuilder newRow = matrixCursor.newRow();
newRow.add(id);// _ID
newRow.add(BaseFile
.genContentIdUriBase(
getAuthority())
.buildUpon().appendPath(f.path)
.build().toString());
newRow.add(f.path);
if (f.displayName == null)
Log.w("KP2AJ", "displayName is null for " + f.path);
newRow.add(f.displayName);
newRow.add(f.canRead ? 1 : 0);
newRow.add(f.canWrite ? 1 : 0);
newRow.add(f.sizeInBytes);
newRow.add(type);
if (f.lastModifiedTime > 0)
newRow.add(f.lastModifiedTime);
else
newRow.add(null);
newRow.add(FileUtils.getResIcon(type, f.displayName));
return newRow;
}
/**
* Retrieves file information of a single file.
*
* @param uri
* the URI pointing to a file.
* @return the file information. Can be {@code null}, based on the input
* parameters.
*/
private MatrixCursor doRetrieveFileInfo(Uri uri) {
Log.d(CLASSNAME, "retrieve file info "+uri.toString());
MatrixCursor matrixCursor = BaseFileProviderUtils.newBaseFileCursor();
String filename = extractFile(uri);
FileEntry f = getFileEntryCached(filename);
if (f == null)
addDeletedFileInfo(matrixCursor, filename);
else
addFileInfo(matrixCursor, 0, f);
return matrixCursor;
}// doRetrieveFileInfo()
//puts the file entry in the cache for later reuse with retrieveFileInfo
private void updateFileEntryCache(FileEntry f) {
if (f != null)
fileEntryMap.put(f.path, f);
}
//removes the file entry from the cache (if cached). Should be called whenever the file changes
private void removeFromCache(String filename, boolean recursive) {
fileEntryMap.remove(filename);
if (recursive)
{
Set<String> keys = fileEntryMap.keySet();
Set<String> keysToRemove = new HashSet<String>();
for (String key: keys)
{
if (key.startsWith(key))
keysToRemove.add(key);
}
for (String key: keysToRemove)
{
fileEntryMap.remove(key);
}
}
}
private void blockFromCache(String filename) {
cacheBlockedFiles.add(filename);
}
private void unblockFromCache(String filename) {
cacheBlockedFiles.remove(filename);
}
//returns the file entry from the cache if present or queries the concrete provider method to return the file info
private FileEntry getFileEntryCached(String filename) {
//check if enry is cached:
FileEntry cachedEntry = fileEntryMap.get(filename);
if (cachedEntry != null)
{
if (Utils.doLog())
Log.d(CLASSNAME, "getFileEntryCached: from cache. " + filename);
return cachedEntry;
}
if (Utils.doLog())
Log.d(CLASSNAME, "getFileEntryCached: not in cache :-( " + filename);
//it's not -> query the information.
FileEntry newEntry = getFileEntry(filename);
if (!cacheBlockedFiles.contains(filename))
updateFileEntryCache(newEntry);
return newEntry;
}
private void addDeletedFileInfo(MatrixCursor matrixCursor, String filename) {
int type = BaseFile.FILE_TYPE_NOT_EXISTED;
RowBuilder newRow = matrixCursor.newRow();
newRow.add(0);// _ID
newRow.add(BaseFile
.genContentIdUriBase(
getAuthority())
.buildUpon().appendPath(filename)
.build().toString());
newRow.add(filename);
newRow.add(filename);
newRow.add(0);
newRow.add(0);
newRow.add(0);
newRow.add(type);
newRow.add(null);
newRow.add(FileUtils.getResIcon(type, filename));
}
/**
* Sorts {@code files}.
*
* @param taskId
* the task ID.
* @param files
* list of files.
* @param ascending
* {@code true} or {@code false}.
* @param sortBy
* can be one of {@link BaseFile.#_SortByModificationTime},
* {@link BaseFile.#_SortByName}, {@link BaseFile.#_SortBySize}.
* @throws Exception
*/
private void sortFiles(final int taskId, final List<FileEntry> files,
final boolean ascending, final int sortBy) throws Exception {
try {
Collections.sort(files, new Comparator<FileEntry>() {
@Override
public int compare(FileEntry lhs, FileEntry rhs) {
if (mMapInterruption.get(taskId))
throw new CancellationException();
if (lhs.isDirectory && !rhs.isDirectory)
return -1;
if (!lhs.isDirectory && rhs.isDirectory)
return 1;
/*
* Default is to compare by name (case insensitive).
*/
int res = mCollator.compare(lhs.path, rhs.path);
switch (sortBy) {
case BaseFile.SORT_BY_NAME:
break;// SortByName
case BaseFile.SORT_BY_SIZE:
if (lhs.sizeInBytes > rhs.sizeInBytes)
res = 1;
else if (lhs.sizeInBytes < rhs.sizeInBytes)
res = -1;
break;// SortBySize
case BaseFile.SORT_BY_MODIFICATION_TIME:
if (lhs.lastModifiedTime > rhs.lastModifiedTime)
res = 1;
else if (lhs.lastModifiedTime < rhs.lastModifiedTime)
res = -1;
break;// SortByDate
}
return ascending ? res : -res;
}// compare()
});
} catch (CancellationException e) {
if (Utils.doLog())
Log.d(CLASSNAME, "sortFiles() >> cancelled...");
}
catch (Exception e)
{
Log.d(CLASSNAME, "sortFiles() >> "+e);
throw e;
}
}// sortFiles()
/**
* Checks ancestor with {@link BaseFile#CMD_IS_ANCESTOR_OF},
* {@link BaseFile#PARAM_SOURCE} and {@link BaseFile#PARAM_TARGET}.
*
* @param uri
* the original URI from client.
* @return {@code null} if source is not ancestor of target; or a
* <i>non-null but empty</i> cursor if the source is.
*/
private MatrixCursor doCheckAncestor(Uri uri) {
String source = Uri.parse(
uri.getQueryParameter(BaseFile.PARAM_SOURCE)).toString();
String target = Uri.parse(
uri.getQueryParameter(BaseFile.PARAM_TARGET)).toString();
if (source == null || target == null)
return null;
boolean validate = ProviderUtils.getBooleanQueryParam(uri,
BaseFile.PARAM_VALIDATE, true);
if (validate) {
//not supported
}
if (!source.endsWith("/"))
source += "/";
String targetParent = getParentPath(target);
if (targetParent != null && targetParent.startsWith(source))
{
if (Utils.doLog())
Log.d(CLASSNAME, source+" is parent of "+target);
return BaseFileProviderUtils.newClosedCursor();
}
if (Utils.doLog())
Log.d(CLASSNAME, source+" is no parent of "+target);
return null;
}// doCheckAncestor()
/**
* Extracts source file from request URI.
*
* @param uri
* the original URI.
* @return the filename.
*/
private static String extractFile(Uri uri) {
String fileName = Uri.parse(uri.getLastPathSegment()).toString();
if (uri.getQueryParameter(BaseFile.PARAM_APPEND_PATH) != null)
fileName += Uri.parse(
uri.getQueryParameter(BaseFile.PARAM_APPEND_PATH)).toString();
if (uri.getQueryParameter(BaseFile.PARAM_APPEND_NAME) != null)
fileName += "/" + uri.getQueryParameter(BaseFile.PARAM_APPEND_NAME);
if (Utils.doLog())
Log.d(CLASSNAME, "extractFile() >> " + fileName);
return fileName;
}// extractFile()
private static String removeTrailingSlash(String path)
{
if (path.endsWith("/")) {
return path.substring(0, path.length() - 1);
}
return path;
}
private String getParentPath(String path)
{
path = removeTrailingSlash(path);
if (path.indexOf("://") == -1)
{
Log.d(CLASSNAME, "invalid path: " + path);
return null;
}
String pathWithoutProtocol = path.substring(path.indexOf("://")+3);
int lastSlashPos = path.lastIndexOf("/");
if (pathWithoutProtocol.indexOf("/") == -1)
{
Log.d(CLASSNAME, "parent of " + path +" is null");
return null;
}
else
{
String parent = path.substring(0, lastSlashPos)+"/";
Log.d(CLASSNAME, "parent of " + path +" is "+parent);
return parent;
}
}
protected abstract FileEntry getFileEntry(String path);
/**
* Lists all file inside {@code dirName}.
*
* @param taskId
* the task ID.
* @param dir
* the source directory.
* @param showHiddenFiles
* {@code true} or {@code false}.
* @param filterMode
* can be one of {@link BaseFile#FILTER_DIRECTORIES_ONLY},
* {@link BaseFile#FILTER_FILES_ONLY},
* {@link BaseFile#FILTER_FILES_AND_DIRECTORIES}.
* @param limit
* the limit.
* @param positiveRegex
* the positive regex filter.
* @param negativeRegex
* the negative regex filter.
* @param results
* the results.
* @param hasMoreFiles
* the first item will contain a value representing that there is
* more files (exceeding {@code limit}) or not.
*/
protected abstract void listFiles(final int taskId, final String dirName,
final boolean showHiddenFiles, final int filterMode,
final int limit, String positiveRegex, String negativeRegex,
final List<FileEntry> results, final boolean hasMoreFiles[]);
protected abstract boolean deletePath(String filename, boolean isRecursive);
protected abstract boolean createDirectory(String dirname, String newDirName);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Some files were not shown because too many files have changed in this diff Show More