changes for NoNet-Release
add project converted to Android Studio
@ -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>
|
||||
|
6
src/java/KP2ASoftkeyboard_AS/.idea/encodings.xml
Normal 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>
|
@ -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>
|
||||
|
12
src/java/KP2ASoftkeyboard_AS/.idea/runConfigurations.xml
Normal 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>
|
1629
src/java/KP2ASoftkeyboard_AS/.idea/workspace.xml
Normal 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" />
|
||||
|
@ -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>
|
@ -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>
|
1
src/java/android-filechooser-AS/.idea/.name
Normal file
@ -0,0 +1 @@
|
||||
code
|
22
src/java/android-filechooser-AS/.idea/compiler.xml
Normal 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>
|
@ -0,0 +1,3 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
19
src/java/android-filechooser-AS/.idea/gradle.xml
Normal 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>
|
@ -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>
|
38
src/java/android-filechooser-AS/.idea/misc.xml
Normal 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>
|
9
src/java/android-filechooser-AS/.idea/modules.xml
Normal 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>
|
1804
src/java/android-filechooser-AS/.idea/workspace.xml
Normal file
19
src/java/android-filechooser-AS/android-filechooser-AS.iml
Normal 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>
|
92
src/java/android-filechooser-AS/app/app.iml
Normal 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>
|
22
src/java/android-filechooser-AS/app/build.gradle
Normal 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'
|
||||
}
|
@ -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>
|
@ -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
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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";
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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";
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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";
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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)$";
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
||||
|
||||
}
|
After Width: | Height: | Size: 511 B |
After Width: | Height: | Size: 453 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 831 B |
After Width: | Height: | Size: 637 B |
After Width: | Height: | Size: 196 B |
After Width: | Height: | Size: 196 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 587 B |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 775 B |
After Width: | Height: | Size: 733 B |
After Width: | Height: | Size: 686 B |
After Width: | Height: | Size: 624 B |
After Width: | Height: | Size: 646 B |
After Width: | Height: | Size: 445 B |
After Width: | Height: | Size: 495 B |