Merge branch '1.0.0e'

This commit is contained in:
Philipp Crocoll 2016-08-30 03:40:17 +02:00
commit 3a3198b2e4
588 changed files with 22803 additions and 919 deletions

View File

@ -10,7 +10,8 @@
<RootNamespace>AndroidFileChooserBinding</RootNamespace> <RootNamespace>AndroidFileChooserBinding</RootNamespace>
<AssemblyName>AndroidFileChooserBinding</AssemblyName> <AssemblyName>AndroidFileChooserBinding</AssemblyName>
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<TargetFrameworkVersion>v5.0</TargetFrameworkVersion> <TargetFrameworkVersion>v6.0</TargetFrameworkVersion>
<AndroidUseLatestPlatformSdk>True</AndroidUseLatestPlatformSdk>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@ -54,6 +55,9 @@
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<LibraryProjectZip Include="..\java\android-filechooser-AS\app\build\outputs\aar\app-debug.aar">
<Link>Jars\app-debug.aar</Link>
</LibraryProjectZip>
<None Include="Jars\AboutJars.txt" /> <None Include="Jars\AboutJars.txt" />
<None Include="Additions\AboutAdditions.txt" /> <None Include="Additions\AboutAdditions.txt" />
<None Include="packages.config" /> <None Include="packages.config" />
@ -71,11 +75,6 @@
<Target Name="AfterBuild"> <Target Name="AfterBuild">
</Target> </Target>
--> -->
<ItemGroup>
<LibraryProjectZip Include="..\java\android-filechooser\code\project.zip">
<Link>project.zip</Link>
</LibraryProjectZip>
</ItemGroup>
<ItemGroup> <ItemGroup>
<XamarinComponentReference Include="xamandroidsupportv4-18"> <XamarinComponentReference Include="xamandroidsupportv4-18">
<Version>20.0.0.4</Version> <Version>20.0.0.4</Version>

View File

@ -10,7 +10,8 @@
<RootNamespace>KP2AKdbLibraryBinding</RootNamespace> <RootNamespace>KP2AKdbLibraryBinding</RootNamespace>
<AssemblyName>KP2AKdbLibraryBinding</AssemblyName> <AssemblyName>KP2AKdbLibraryBinding</AssemblyName>
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<TargetFrameworkVersion>v5.0</TargetFrameworkVersion> <TargetFrameworkVersion>v6.0</TargetFrameworkVersion>
<AndroidUseLatestPlatformSdk>True</AndroidUseLatestPlatformSdk>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>

View File

@ -254,7 +254,8 @@ namespace keepass2android.Io
//check if we need to request the external-storage-permission at runtime //check if we need to request the external-storage-permission at runtime
if (ioc.IsLocalFile()) if (ioc.IsLocalFile())
{ {
bool requiresPermission = !ioc.Path.StartsWith(activity.Activity.FilesDir.CanonicalPath); bool requiresPermission = !(ioc.Path.StartsWith(activity.Activity.FilesDir.CanonicalPath)
|| ioc.Path.StartsWith(IoUtil.GetInternalDirectory(activity.Activity).CanonicalPath));
var extDirectory = activity.Activity.GetExternalFilesDir(null); var extDirectory = activity.Activity.GetExternalFilesDir(null);
if ((extDirectory != null) && (ioc.Path.StartsWith(extDirectory.CanonicalPath))) if ((extDirectory != null) && (ioc.Path.StartsWith(extDirectory.CanonicalPath)))

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Android.Content; using Android.Content;
using Android.OS;
using Java.IO; using Java.IO;
using KeePassLib.Serialization; using KeePassLib.Serialization;
@ -53,13 +54,14 @@ namespace keepass2android.Io
try try
{ {
File filesDir = context.FilesDir.CanonicalFile; File filesDir = context.FilesDir.CanonicalFile;
File noBackupDir = GetInternalDirectory(context).CanonicalFile;
File ourFile = new File(path).CanonicalFile; File ourFile = new File(path).CanonicalFile;
//http://www.java2s.com/Tutorial/Java/0180__File/Checkswhetherthechilddirectoryisasubdirectoryofthebasedirectory.htm //http://www.java2s.com/Tutorial/Java/0180__File/Checkswhetherthechilddirectoryisasubdirectoryofthebasedirectory.htm
File parentFile = ourFile; File parentFile = ourFile;
while (parentFile != null) while (parentFile != null)
{ {
if (filesDir.Equals(parentFile)) if ((filesDir.Equals(parentFile) || noBackupDir.Equals(parentFile)))
{ {
return true; return true;
} }
@ -92,5 +94,13 @@ namespace keepass2android.Io
writeTransaction.CommitWrite(); writeTransaction.CommitWrite();
} }
} }
public static Java.IO.File GetInternalDirectory(Context ctx)
{
if ((int)Android.OS.Build.VERSION.SdkInt >= 21)
return ctx.NoBackupFilesDir;
else
return ctx.FilesDir;
}
} }
} }

View File

@ -94,7 +94,6 @@
<Compile Include="Io\IFileStorage.cs" /> <Compile Include="Io\IFileStorage.cs" />
<Compile Include="Io\IoUtil.cs" /> <Compile Include="Io\IoUtil.cs" />
<Compile Include="Io\JavaFileStorage.cs" /> <Compile Include="Io\JavaFileStorage.cs" />
<Compile Include="Io\NetFtpFileStorage.cs" />
<Compile Include="Io\OfflineSwitchableFileStorage.cs" /> <Compile Include="Io\OfflineSwitchableFileStorage.cs" />
<Compile Include="Io\SftpFileStorage.cs" /> <Compile Include="Io\SftpFileStorage.cs" />
<Compile Include="Io\SkyDriveFileStorage.cs" /> <Compile Include="Io\SkyDriveFileStorage.cs" />

View File

@ -101,7 +101,7 @@ namespace keepass2android
} }
catch (DuplicateUuidsException e) catch (DuplicateUuidsException e)
{ {
Kp2aLog.LogUnexpectedError(e); Kp2aLog.Log(e.ToString());
Finish(false, _app.GetResourceString(UiStringKey.DuplicateUuidsError) + " " + e.Message + _app.GetResourceString(UiStringKey.DuplicateUuidsErrorAdditional), Exception); Finish(false, _app.GetResourceString(UiStringKey.DuplicateUuidsError) + " " + e.Message + _app.GetResourceString(UiStringKey.DuplicateUuidsErrorAdditional), Exception);
return; return;
} }

@ -1 +1 @@
Subproject commit 1495b1ad7914fa2be69e111b90039770d101aa3d Subproject commit 6d376e4caca9131ea841e985d7cda85ac5906d67

View File

@ -0,0 +1,11 @@
<component name="libraryTable">
<library name="1_commons-logging-1.1.1">
<CLASSES>
<root url="jar://$APPLICATION_HOME_DIR$/gradle/m2repository/commons-logging/commons-logging/1.1.1/commons-logging-1.1.1.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$APPLICATION_HOME_DIR$/gradle/m2repository/commons-logging/commons-logging/1.1.1/commons-logging-1.1.1-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -0,0 +1,11 @@
<component name="libraryTable">
<library name="1_httpcore-4.0.1">
<CLASSES>
<root url="jar://$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents/httpcore/4.0.1/e813b8722c387b22e1adccf7914729db09bcb4a9/httpcore-4.0.1.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents/httpcore/4.0.1/69febd50593a006a4593ef64a697b14208a55b7b/httpcore-4.0.1-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -0,0 +1,11 @@
<component name="libraryTable">
<library name="commons-codec-1.3">
<CLASSES>
<root url="jar://$USER_HOME$/.gradle/caches/modules-2/files-2.1/commons-codec/commons-codec/1.3/fd32786786e2adb664d5ecc965da47629dca14ba/commons-codec-1.3.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$USER_HOME$/.gradle/caches/modules-2/files-2.1/commons-codec/commons-codec/1.3/ee264b5d7a0f939d5999398c7f1ade031db3fcec/commons-codec-1.3-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -0,0 +1,11 @@
<component name="libraryTable">
<library name="httpclient-4.0.1">
<CLASSES>
<root url="jar://$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents/httpclient/4.0.1/1d7d28fa738bdbfe4fbd895d9486308999bdf440/httpclient-4.0.1.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents/httpclient/4.0.1/2480b1ac944ebdd15f5f7b6730e9a151d39f4d88/httpclient-4.0.1-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -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>

View File

@ -1 +1 @@
java KP2ASoftkeyboard_AS

View File

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

View File

@ -5,7 +5,7 @@
<GradleProjectSettings> <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>

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" /> <excludeFolder url="file://$MODULE_DIR$/.gradle" />
</content> </content>
<orderEntry type="jdk" jdkName="1.7" jdkType="JavaSDK" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>

View 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" />

View File

@ -200,7 +200,7 @@ public class AutoFillService extends AccessibilityService {
{ {
android.util.Log.e(_logTag, (e.toString() == null) ? "(null)" : e.toString() ); android.util.Log.e(_logTag, (e.toString() == null) ? "(null)" : e.toString() );
Intent intent = new Intent(Intent.ACTION_SEND); /*Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("message/rfc822"); intent.setType("message/rfc822");
String to = "crocoapps@gmail.com"; String to = "crocoapps@gmail.com";
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{to}); intent.putExtra(Intent.EXTRA_EMAIL, new String[]{to});
@ -217,7 +217,7 @@ public class AutoFillService extends AccessibilityService {
.setContentIntent(PendingIntent.getActivity(this, 0, Intent.createChooser(intent, "Send error report"), PendingIntent.FLAG_CANCEL_CURRENT)); .setContentIntent(PendingIntent.getActivity(this, 0, Intent.createChooser(intent, "Send error report"), PendingIntent.FLAG_CANCEL_CURRENT));
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notificationManager.notify(autoFillNotificationId+1, builder.build()); notificationManager.notify(autoFillNotificationId+1, builder.build());*/
} }
} }

View File

@ -6,5 +6,15 @@
<string name="open_entry_for_app">Αναζήτηση για καταχώρηση με \"%1$s\"</string> <string name="open_entry_for_app">Αναζήτηση για καταχώρηση με \"%1$s\"</string>
<string name="kp2a_user">Χρήστης</string> <string name="kp2a_user">Χρήστης</string>
<string name="kp2a_password">Κωδικός Πρόσβασης</string> <string name="kp2a_password">Κωδικός Πρόσβασης</string>
<string name="kp2a_prefs">Ρυθμίσεις των διαπιστευτηρίων εισόδου</string>
<string name="kp2a_auto_fill">Αυτόματη συμπλήρωση ενεργή</string>
<string name="kp2a_auto_fill_summary">Συμπληρώστε αυτόματα το κείμενο όταν εισάγεται ένα κενό πεδίο, εάν μια καταχώρηση του Keepass2Android είναι διαθέσιμη για το πληκτρολόγιο και να υπάρχει μια τιμή που ταιριάζει με το κείμενο υποδείξεων του πεδίου.</string>
<string name="kp2a_remember_auto_fill">Να θυμάται τις υποδείξεις του πεδίου</string>
<string name="kp2a_remember_auto_fill_summary">Εάν ένα πεδίο κειμένου συμπληρωθεί επιλέγοντας χειροκίνητα την τιμή Keepass2Android, να θυμάται την τιμή που πληκτρολογήθηκε στο πεδίο κειμένου. Το πεδίο κειμένου αργότερα μπορεί να εντοπιστεί ξανά από το κείμενο υποδείξεων.</string>
<string name="kp2a_simple_keyboard">Απλό πληκτρολόγιο</string> <string name="kp2a_simple_keyboard">Απλό πληκτρολόγιο</string>
<string name="kp2a_simple_keyboard_summary">Εμφάνισε το απλό πληκτρολόγιο μιας γραμμής αν μια καταχώριση είναι διαθέσιμη. Αν είναι απενεργοποιημένο τότε ένα παράθυρο διαλόγου θα εμφανιστεί όταν πατηθεί το πλήκτρο Keepass2Android.</string>
<string name="kp2a_lock_on_sendgodone">Κλείδωμα της βάσης δεδομένων μετά τη χρήση</string>
<string name="kp2a_lock_on_sendgodone_summary">Όταν πατηθεί το πλήκτρο Ολοκλήρωση/Αποστολή/Go στο απλό πληκτρολόγιο μιας σειράς, αυτόματα να κλειδωθεί η βάση δεδομένων.</string>
<string name="kp2a_switch_on_sendgodone">Εναλλαγή πληκτρολογίου μετά τη χρήση</string>
<string name="kp2a_switch_on_sendgodone_summary">Όταν πατηθεί το πλήκτρο Ολοκλήρωση/Αποστολή/Go στο απλό πληκτρολόγιο μιας σειράς, αυτόματα να αλλαχθεί το πληκτρολόγιο.</string>
</resources> </resources>

View File

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

View File

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

View File

@ -6,9 +6,9 @@
<string name="open_entry_for_app">Cerca la voce \"%1$s\"</string> <string name="open_entry_for_app">Cerca la voce \"%1$s\"</string>
<string name="kp2a_user">Utente</string> <string name="kp2a_user">Utente</string>
<string name="kp2a_password">Password</string> <string name="kp2a_password">Password</string>
<string name="kp2a_prefs">Impostazioni delle credenziali di ingresso</string> <string name="kp2a_prefs">Impostazioni di inserimento credenziali</string>
<string name="kp2a_auto_fill">Auto-Compilazione abilitata</string> <string name="kp2a_auto_fill">Completamento automatico abilitato</string>
<string name="kp2a_auto_fill_summary">Inserisci automaticamente il testo, quando si entra in un campo vuoto, se è selezionata una voce di Keepass2Android per la tastiera ed esiste un valore che corrisponde al testo di suggerimento del campo.</string> <string name="kp2a_auto_fill_summary">Inserisci automaticamente il testo quando si entra in un campo vuoto, se è disponibile per la tastiera una voce di Keepass2Android ed esiste un valore che corrisponde al testo di suggerimento del campo.</string>
<string name="kp2a_remember_auto_fill">Ricorda i testi di suggerimento del campo</string> <string name="kp2a_remember_auto_fill">Ricorda i testi di suggerimento del campo</string>
<string name="kp2a_remember_auto_fill_summary">Se un campo di testo viene compilato selezionando manualmente il valore di Keepass2Android, ricorda quale valore è stato immesso nel campo. In seguito il campo di testo verrà rilevato tramite il suo testo di suggerimento.</string> <string name="kp2a_remember_auto_fill_summary">Se un campo di testo viene compilato selezionando manualmente il valore di Keepass2Android, ricorda quale valore è stato immesso nel campo. In seguito il campo di testo verrà rilevato tramite il suo testo di suggerimento.</string>
<string name="kp2a_simple_keyboard">Tastiera semplice</string> <string name="kp2a_simple_keyboard">Tastiera semplice</string>

View File

@ -8,6 +8,7 @@
<string name="kp2a_password">סיסמה</string> <string name="kp2a_password">סיסמה</string>
<string name="kp2a_prefs">הגדרות קלט האימות</string> <string name="kp2a_prefs">הגדרות קלט האימות</string>
<string name="kp2a_auto_fill">השלמה-אוטומאטית מאופשרת</string> <string name="kp2a_auto_fill">השלמה-אוטומאטית מאופשרת</string>
<string name="kp2a_auto_fill_summary">מילוי אוטומטי בטקסט בעת הזנת שדה ריק, אם ערך Keepass2Android אינו זמין עבור לוח המקשים, יש ערך אשר תואם את טקסט הרמז של השדה.</string>
<string name="kp2a_remember_auto_fill">זכור את שדה טקסט הרמזים</string> <string name="kp2a_remember_auto_fill">זכור את שדה טקסט הרמזים</string>
<string name="kp2a_simple_keyboard">מקלדת פשוטה</string> <string name="kp2a_simple_keyboard">מקלדת פשוטה</string>
<string name="kp2a_lock_on_sendgodone">נעל מסד הנתונים בסיום</string> <string name="kp2a_lock_on_sendgodone">נעל מסד הנתונים בסיום</string>

View File

@ -3,18 +3,18 @@
<resources> <resources>
<string name="change_entry">別のエントリを選択</string> <string name="change_entry">別のエントリを選択</string>
<string name="open_entry">エントリの選択</string> <string name="open_entry">エントリの選択</string>
<string name="open_entry_for_app">エントリを\"%1$s\"で検索します。</string> <string name="open_entry_for_app">エントリを\"%1$s\"で検索</string>
<string name="kp2a_user">ユーザー</string> <string name="kp2a_user">ユーザー</string>
<string name="kp2a_password">パスワード</string> <string name="kp2a_password">パスワード</string>
<string name="kp2a_prefs">資格情報の入力の設定</string> <string name="kp2a_prefs">資格情報の入力の設定</string>
<string name="kp2a_auto_fill">オートフィルを有効にする</string> <string name="kp2a_auto_fill">オートフィルを有効にする</string>
<string name="kp2a_auto_fill_summary">Keepass2Android のエントリーでキーボードが使用可能で、フィールドのヒントテキストに一致する値がある場合、空のフィールドに自動的にテキストが入力されます。</string> <string name="kp2a_auto_fill_summary">Keepass2Android のエントリーでキーボードが使用可能で、フィールドのヒントテキストに一致する値がある場合、空のフィールドに自動的にテキストが入力されます。</string>
<string name="kp2a_remember_auto_fill">フィールドのヒントテキストを保存</string> <string name="kp2a_remember_auto_fill">フィールドのヒントテキストを保存</string>
<string name="kp2a_remember_auto_fill_summary">Keepass2Android の値を手動で選択してテキスト フィールドを入力する場合、テキスト フィールドに入力された値を保存します。後でそのヒント テキストを使用してテキスト フィールドを検出します。</string> <string name="kp2a_remember_auto_fill_summary">Keepass2Android の値を手動で選択してテキストフィールドを入力する場合、テキストフィールドに入力された値を保存します。後でそのヒントテキストを使用してテキストフィールドを検出します。</string>
<string name="kp2a_simple_keyboard">シンプルキーボード</string> <string name="kp2a_simple_keyboard">シンプルキーボード</string>
<string name="kp2a_simple_keyboard_summary">エントリがキーボードで利用できる場合シンプルな1行のキーボードをを表示します。もし無効にした場合、Keepass2Androidキーが押されたときにダイアログが表示されます。</string> <string name="kp2a_simple_keyboard_summary">エントリーがキーボードで利用できる場合にシンプルな1行のキーボードを表示します。無効の場合、Keepass2Androidキーが押されるとダイアログが表示されます。</string>
<string name="kp2a_lock_on_sendgodone">完了時にデータベースをロックする</string> <string name="kp2a_lock_on_sendgodone">完了時にデータベースをロックする</string>
<string name="kp2a_lock_on_sendgodone_summary">シンプルな1行のキーボードで完了/送信/実行キーを押したときにデータベースを自動でロックします。</string> <string name="kp2a_lock_on_sendgodone_summary">シンプルな1行のキーボードで完了/送信/実行キーを押したときにデータベースを自動でロックします。</string>
<string name="kp2a_switch_on_sendgodone">完了時にキーボードを切り替える</string> <string name="kp2a_switch_on_sendgodone">完了時にキーボードを切り替える</string>
<string name="kp2a_switch_on_sendgodone_summary">シンプルな1行のキーボードで完了/送信/実行キーを押したときにキーボードを切り替えます。</string> <string name="kp2a_switch_on_sendgodone_summary">シンプルな1行のキーボードで完了/送信/実行キーを押したときにキーボードを切り替えます。</string>
</resources> </resources>

View File

@ -6,7 +6,7 @@
<string name="open_entry_for_app">Zoek voor regel met \"%1$s\"</string> <string name="open_entry_for_app">Zoek voor regel met \"%1$s\"</string>
<string name="kp2a_user">Gebruiker</string> <string name="kp2a_user">Gebruiker</string>
<string name="kp2a_password">Wachtwoord</string> <string name="kp2a_password">Wachtwoord</string>
<string name="kp2a_prefs">Referentie invoer instellingen</string> <string name="kp2a_prefs">Instellingen voor invoer van logingegevens</string>
<string name="kp2a_auto_fill">Automatisch-vullen ingeschakeld</string> <string name="kp2a_auto_fill">Automatisch-vullen ingeschakeld</string>
<string name="kp2a_auto_fill_summary">Vult automatisch tekst in een leeg tekstveld in, als een Keepass2Android regel beschikbaar is voor het toetsenbord en als het veld overeenkomt met de opgeslagen veld hint-tekst.</string> <string name="kp2a_auto_fill_summary">Vult automatisch tekst in een leeg tekstveld in, als een Keepass2Android regel beschikbaar is voor het toetsenbord en als het veld overeenkomt met de opgeslagen veld hint-tekst.</string>
<string name="kp2a_remember_auto_fill">Onthoud veld hint-teksten</string> <string name="kp2a_remember_auto_fill">Onthoud veld hint-teksten</string>
@ -14,7 +14,7 @@
<string name="kp2a_simple_keyboard">Eenvoudig toetsenbord</string> <string name="kp2a_simple_keyboard">Eenvoudig toetsenbord</string>
<string name="kp2a_simple_keyboard_summary">Toon het eenvoudige toetsenbord als een KP2A regel beschikbaar is voor het toetsenbord. Wanneer uitgeschakeld, een venster word getoond als de Keepass2Android toets is ingedrukt.</string> <string name="kp2a_simple_keyboard_summary">Toon het eenvoudige toetsenbord als een KP2A regel beschikbaar is voor het toetsenbord. Wanneer uitgeschakeld, een venster word getoond als de Keepass2Android toets is ingedrukt.</string>
<string name="kp2a_lock_on_sendgodone">Vergrendel de database na voltooiing</string> <string name="kp2a_lock_on_sendgodone">Vergrendel de database na voltooiing</string>
<string name="kp2a_lock_on_sendgodone_summary">Als de Gedaan/Verzenden/Gaan toets op het eenvoudige toetsenbord is ingedrukt, vergrendel automatisch de database.</string> <string name="kp2a_lock_on_sendgodone_summary">Als de Gedaan/Verzenden/Gaan toets op het eenvoudige toetsenbord is ingedrukt, vergrendel dan automatisch de database.</string>
<string name="kp2a_switch_on_sendgodone">Wissel toetsenbord na voltooiing</string> <string name="kp2a_switch_on_sendgodone">Wissel toetsenbord na voltooiing</string>
<string name="kp2a_switch_on_sendgodone_summary">Als de Gedaan/Verzenden/Gaan toets op het eenvoudige toetsenbord is ingedrukt, verwissel het toetsenbord.</string> <string name="kp2a_switch_on_sendgodone_summary">Als de Gedaan/Verzenden/Gaan toets op het eenvoudige toetsenbord is ingedrukt, verwissel dan het toetsenbord.</string>
</resources> </resources>

View File

@ -7,7 +7,7 @@
<string name="kp2a_user">Пользователь</string> <string name="kp2a_user">Пользователь</string>
<string name="kp2a_password">Пароль</string> <string name="kp2a_password">Пароль</string>
<string name="kp2a_prefs">Параметры ввода учетных данных</string> <string name="kp2a_prefs">Параметры ввода учетных данных</string>
<string name="kp2a_auto_fill">Автозаполнение включено</string> <string name="kp2a_auto_fill">АвтоЗаполнение включено</string>
<string name="kp2a_auto_fill_summary">Автоматически заполняет текст при входе в пустое поле, если запись Keepass2Android доступна для клавиатуры и есть значение, соответствующее тексту подсказки для поля.</string> <string name="kp2a_auto_fill_summary">Автоматически заполняет текст при входе в пустое поле, если запись Keepass2Android доступна для клавиатуры и есть значение, соответствующее тексту подсказки для поля.</string>
<string name="kp2a_remember_auto_fill">Запоминать тексты подсказки для полей</string> <string name="kp2a_remember_auto_fill">Запоминать тексты подсказки для полей</string>
<string name="kp2a_remember_auto_fill_summary">При ручном заполнении текстового поля выбором значения Keepass2Android запоминает, какое значение было введено в текстовое поле. <string name="kp2a_remember_auto_fill_summary">При ручном заполнении текстового поля выбором значения Keepass2Android запоминает, какое значение было введено в текстовое поле.

View File

@ -1,14 +1,20 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--Generated by crowdin.com--> <!--Generated by crowdin.com-->
<resources> <resources>
<string name="change_entry">Başka bir kayıt seçin</string> <string name="change_entry">Başka bir kayıt seç</string>
<string name="open_entry">Kayıt seçin</string> <string name="open_entry">Kayıt seç</string>
<string name="open_entry_for_app">Kayıt \"%1$s\" ile arama</string> <string name="open_entry_for_app">\"%1$s\" ile kayıt arama</string>
<string name="kp2a_user">Kullanıcı</string> <string name="kp2a_user">Kullanıcı</string>
<string name="kp2a_password">Şifre</string> <string name="kp2a_password">Parola</string>
<string name="kp2a_prefs">Giriş kimlik bilgisi ayarları</string> <string name="kp2a_prefs">Giriş kimlik bilgisi ayarları</string>
<string name="kp2a_auto_fill">Otomatik doldurma etkin</string> <string name="kp2a_auto_fill">Otomatik doldurma etkin</string>
<string name="kp2a_auto_fill_summary">Klavyede Keepass2Android kaydı mevcutsa ve bu değer bir alan ipucu metniyle eşleşiyorsa, boş alan otomatik olarak doldurulur.</string>
<string name="kp2a_remember_auto_fill">Alan ipucu metinlerini hatırla</string>
<string name="kp2a_remember_auto_fill_summary">Bir metin alanına Keepass2Android\'den değer seçerek doldurursanız, metin alanına girilen değer hatırlanır. Metin alanı, ipucu metninden daha sonra yeniden tespit edilir.</string>
<string name="kp2a_simple_keyboard">Basit klavye</string> <string name="kp2a_simple_keyboard">Basit klavye</string>
<string name="kp2a_lock_on_sendgodone">İşiniz bittiğinde veritabanı kilitlensin</string> <string name="kp2a_simple_keyboard_summary">Kayıt uygunsa tek satırlı klavyeyi gösterir. Devre dışı bırakılırsa, klavyedeki Keepass2Android tuşuna basıldığında bir pencere şeklinde gözükür.</string>
<string name="kp2a_switch_on_sendgodone">İşiniz bittiğinde klavyeyi değiştir</string> <string name="kp2a_lock_on_sendgodone">Bittiğinde veritabanını kilitle</string>
<string name="kp2a_lock_on_sendgodone_summary">1 satırlı basit klavyede Tamam/Gönder/Git tuşlarına basıldığında, veri tabanını otomatik kilitler.</string>
<string name="kp2a_switch_on_sendgodone">Bittiğinde klavyeyi değiştir</string>
<string name="kp2a_switch_on_sendgodone_summary">1 satırlı basit klavyede Tamam/Gönder/Git tuşlarına basıldığında, klavyeyi değiştirir.</string>
</resources> </resources>

View File

@ -3,18 +3,18 @@
<resources> <resources>
<string name="change_entry">Вибрати інший запис</string> <string name="change_entry">Вибрати інший запис</string>
<string name="open_entry">Вибрати запис</string> <string name="open_entry">Вибрати запис</string>
<string name="open_entry_for_app">Пошук запису з \'%1$s\'</string> <string name="open_entry_for_app">Пошук запису з \"%1$s\"</string>
<string name="kp2a_user">Користувач</string> <string name="kp2a_user">Користувач</string>
<string name="kp2a_password">Пароль</string> <string name="kp2a_password">Пароль</string>
<string name="kp2a_prefs">Налаштування вводу облікових даних</string> <string name="kp2a_prefs">Налаштування вводу облікових даних</string>
<string name="kp2a_auto_fill">Автозаповнення увімкнуте</string> <string name="kp2a_auto_fill">Автозаповнення увімкнено</string>
<string name="kp2a_auto_fill_summary">Автоматично заповнює текст, коли введене порожнє поле, якщо запис Keepass2Android є доступним для клавіатури та існує значення, що відповідає тексту підказки поля.</string> <string name="kp2a_auto_fill_summary">Автоматично заповнює текст при вході в порожнє поле, якщо запис Keepass2Android є доступним для клавіатури та існує значення, що відповідає тексту підказки для поля.</string>
<string name="kp2a_remember_auto_fill">Запам\'ятати тексти підказки для полів</string> <string name="kp2a_remember_auto_fill">Запам\'ятовувати тексти підказки для полів</string>
<string name="kp2a_remember_auto_fill_summary">Якщо текстове поле заповнене значенням Keepass2Android, що було вибране вручну, запам\'ятовувати, яке значення було введене в текстове поле. Наступного разу це поле буде визначене за його текстом підказки.</string> <string name="kp2a_remember_auto_fill_summary">При ручному заповненні текстового поля вибором значення, Keepass2Android запам\'ятовує, яке значення було введене в текстове поле. Надалі текстове поле буде визначатися за текстом його підказки.</string>
<string name="kp2a_simple_keyboard">Проста клавіатура</string> <string name="kp2a_simple_keyboard">Проста клавіатура</string>
<string name="kp2a_simple_keyboard_summary">Показувати просту 1-рядну клавіатуру, якщо запис є доступним для клавіатури. Якщо вимкнено, відкривається діалог, коли натискається кнопка Keepass2Android.</string> <string name="kp2a_simple_keyboard_summary">Показувати просту 1-рядну клавіатуру, якщо запис є доступним для клавіатури. Якщо вимкнено, діалогове вікно відкривається при натисканні кнопки Keepass2Android.</string>
<string name="kp2a_lock_on_sendgodone">Блокувати базу даних після завершення</string> <string name="kp2a_lock_on_sendgodone">Блокувати базу паролів після завершення</string>
<string name="kp2a_lock_on_sendgodone_summary">При натисканні кнопки Виконано/Відправити/Перейти на простій 1-рядній клавіатурі, автоматично блокувати базу даних.</string> <string name="kp2a_lock_on_sendgodone_summary">При натисканні кнопки Виконано/Відправити/Перейти на простій 1-рядній клавіатурі, автоматично блокувати базу паролів.</string>
<string name="kp2a_switch_on_sendgodone">Змінити клавіатуру після завершення</string> <string name="kp2a_switch_on_sendgodone">Змінити клавіатуру після завершення</string>
<string name="kp2a_switch_on_sendgodone_summary">При натисканні кнопки Виконано/Відправити/Перейти на простій 1-рядній клавіатурі, змінити клавіатуру.</string> <string name="kp2a_switch_on_sendgodone_summary">При натисканні кнопки Виконано/Відправити/Перейти на простій 1-рядній клавіатурі, змінити клавіатуру.</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--Generated by crowdin.net--> <!--Generated by crowdin.com-->
<resources> <resources>
<string name="change_entry">Chọn mục khác</string> <string name="change_entry">Chọn mục khác</string>
<string name="open_entry">Chọn mục</string> <string name="open_entry">Chọn mục</string>

View File

@ -0,0 +1 @@
code

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" /> <mapping directory="" vcs="" />
</component> </component>
</project> </project>

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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