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="httpcore-4.0.1" level="project" /> | ||||||
|     <orderEntry type="library" exported="" name="json_simple-1.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-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="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="google-http-client-jackson-1.16.0-rc" level="project" /> | ||||||
|     <orderEntry type="library" exported="" name="commons-logging-1.1.1" level="project" /> |     <orderEntry type="library" exported="" name="commons-logging-1.1.1" level="project" /> | ||||||
|   </component> |   </component> | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								src/java/KP2ASoftkeyboard_AS/.idea/encodings.xml
									
									
									
										generated
									
									
									
										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> | ||||||
							
								
								
									
										8
									
								
								src/java/KP2ASoftkeyboard_AS/.idea/gradle.xml
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -5,7 +5,7 @@ | |||||||
|       <GradleProjectSettings> |       <GradleProjectSettings> | ||||||
|         <option name="distributionType" value="LOCAL" /> |         <option name="distributionType" value="LOCAL" /> | ||||||
|         <option name="externalProjectPath" value="$PROJECT_DIR$" /> |         <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="gradleJvm" value="1.7" /> | ||||||
|         <option name="modules"> |         <option name="modules"> | ||||||
|           <set> |           <set> | ||||||
| @@ -13,6 +13,12 @@ | |||||||
|             <option value="$PROJECT_DIR$/app" /> |             <option value="$PROJECT_DIR$/app" /> | ||||||
|           </set> |           </set> | ||||||
|         </option> |         </option> | ||||||
|  |         <option name="myModules"> | ||||||
|  |           <set> | ||||||
|  |             <option value="$PROJECT_DIR$" /> | ||||||
|  |             <option value="$PROJECT_DIR$/app" /> | ||||||
|  |           </set> | ||||||
|  |         </option> | ||||||
|       </GradleProjectSettings> |       </GradleProjectSettings> | ||||||
|     </option> |     </option> | ||||||
|   </component> |   </component> | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								src/java/KP2ASoftkeyboard_AS/.idea/runConfigurations.xml
									
									
									
										generated
									
									
									
										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
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						| @@ -12,10 +12,8 @@ | |||||||
|         <option name="SELECTED_TEST_ARTIFACT" value="_android_test_" /> |         <option name="SELECTED_TEST_ARTIFACT" value="_android_test_" /> | ||||||
|         <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" /> |         <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" /> | ||||||
|         <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" /> |         <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="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugAndroidTest" /> | ||||||
|         <option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileDebugAndroidTestSources" /> |         <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="ALLOW_USER_CONFIGURATION" value="false" /> | ||||||
|         <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" /> |         <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" /> | ||||||
|         <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" /> |         <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
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | code | ||||||
							
								
								
									
										22
									
								
								src/java/android-filechooser-AS/.idea/compiler.xml
									
									
									
										generated
									
									
									
										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> | ||||||
							
								
								
									
										3
									
								
								src/java/android-filechooser-AS/.idea/copyright/profiles_settings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | |||||||
|  | <component name="CopyrightManager"> | ||||||
|  |   <settings default="" /> | ||||||
|  | </component> | ||||||
							
								
								
									
										19
									
								
								src/java/android-filechooser-AS/.idea/gradle.xml
									
									
									
										generated
									
									
									
										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> | ||||||
							
								
								
									
										11
									
								
								src/java/android-filechooser-AS/.idea/libraries/support_v4_18_0_0.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | |||||||
|  | <component name="libraryTable"> | ||||||
|  |   <library name="support-v4-18.0.0"> | ||||||
|  |     <CLASSES> | ||||||
|  |       <root url="jar://$USER_HOME$/AppData/Local/Android/android-sdk/extras/android/m2repository/com/android/support/support-v4/18.0.0/support-v4-18.0.0.jar!/" /> | ||||||
|  |     </CLASSES> | ||||||
|  |     <JAVADOC /> | ||||||
|  |     <SOURCES> | ||||||
|  |       <root url="jar://$USER_HOME$/AppData/Local/Android/android-sdk/extras/android/m2repository/com/android/support/support-v4/18.0.0/support-v4-18.0.0-sources.jar!/" /> | ||||||
|  |     </SOURCES> | ||||||
|  |   </library> | ||||||
|  | </component> | ||||||
							
								
								
									
										38
									
								
								src/java/android-filechooser-AS/.idea/misc.xml
									
									
									
										generated
									
									
									
										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
									
									
									
										generated
									
									
									
										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
									
									
									
										generated
									
									
									
										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 | 
 Philipp Crocoll
					Philipp Crocoll