diff --git a/src/java/KP2ASoftkeyboard_AS/app/build/outputs/aar/app-debug.aar b/src/java/KP2ASoftkeyboard_AS/app/build/outputs/aar/app-debug.aar
index cc0cbd44..4424afd0 100644
Binary files a/src/java/KP2ASoftkeyboard_AS/app/build/outputs/aar/app-debug.aar and b/src/java/KP2ASoftkeyboard_AS/app/build/outputs/aar/app-debug.aar differ
diff --git a/src/java/KP2ASoftkeyboard_AS/app/src/main/java/keepass2android/autofill/AutoFillService.java b/src/java/KP2ASoftkeyboard_AS/app/src/main/java/keepass2android/autofill/AutoFillService.java
index 6d6b5650..ae1781e7 100644
--- a/src/java/KP2ASoftkeyboard_AS/app/src/main/java/keepass2android/autofill/AutoFillService.java
+++ b/src/java/KP2ASoftkeyboard_AS/app/src/main/java/keepass2android/autofill/AutoFillService.java
@@ -200,7 +200,7 @@ public class AutoFillService extends AccessibilityService {
{
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");
String to = "crocoapps@gmail.com";
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));
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- notificationManager.notify(autoFillNotificationId+1, builder.build());
+ notificationManager.notify(autoFillNotificationId+1, builder.build());*/
}
}
diff --git a/src/java/JavaFileStorage/app/src/main/java/keepass2android/yubiclip/scancode/KeyboardLayout.java b/src/java/KP2ASoftkeyboard_AS/app/src/main/java/keepass2android/yubiclip/scancode/KeyboardLayout.java
similarity index 100%
rename from src/java/JavaFileStorage/app/src/main/java/keepass2android/yubiclip/scancode/KeyboardLayout.java
rename to src/java/KP2ASoftkeyboard_AS/app/src/main/java/keepass2android/yubiclip/scancode/KeyboardLayout.java
diff --git a/src/java/JavaFileStorage/app/src/main/java/keepass2android/yubiclip/scancode/USKeyboardLayout.java b/src/java/KP2ASoftkeyboard_AS/app/src/main/java/keepass2android/yubiclip/scancode/USKeyboardLayout.java
similarity index 100%
rename from src/java/JavaFileStorage/app/src/main/java/keepass2android/yubiclip/scancode/USKeyboardLayout.java
rename to src/java/KP2ASoftkeyboard_AS/app/src/main/java/keepass2android/yubiclip/scancode/USKeyboardLayout.java
diff --git a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-el/strings_kp2a.xml b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-el/strings_kp2a.xml
index f428f208..72527277 100644
--- a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-el/strings_kp2a.xml
+++ b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-el/strings_kp2a.xml
@@ -6,5 +6,15 @@
Αναζήτηση για καταχώρηση με \"%1$s\"ΧρήστηςΚωδικός Πρόσβασης
+ Ρυθμίσεις των διαπιστευτηρίων εισόδου
+ Αυτόματη συμπλήρωση ενεργή
+ Συμπληρώστε αυτόματα το κείμενο όταν εισάγεται ένα κενό πεδίο, εάν μια καταχώρηση του Keepass2Android είναι διαθέσιμη για το πληκτρολόγιο και να υπάρχει μια τιμή που ταιριάζει με το κείμενο υποδείξεων του πεδίου.
+ Να θυμάται τις υποδείξεις του πεδίου
+ Εάν ένα πεδίο κειμένου συμπληρωθεί επιλέγοντας χειροκίνητα την τιμή Keepass2Android, να θυμάται την τιμή που πληκτρολογήθηκε στο πεδίο κειμένου. Το πεδίο κειμένου αργότερα μπορεί να εντοπιστεί ξανά από το κείμενο υποδείξεων.Απλό πληκτρολόγιο
+ Εμφάνισε το απλό πληκτρολόγιο μιας γραμμής αν μια καταχώριση είναι διαθέσιμη. Αν είναι απενεργοποιημένο τότε ένα παράθυρο διαλόγου θα εμφανιστεί όταν πατηθεί το πλήκτρο Keepass2Android.
+ Κλείδωμα της βάσης δεδομένων μετά τη χρήση
+ Όταν πατηθεί το πλήκτρο Ολοκλήρωση/Αποστολή/Go στο απλό πληκτρολόγιο μιας σειράς, αυτόματα να κλειδωθεί η βάση δεδομένων.
+ Εναλλαγή πληκτρολογίου μετά τη χρήση
+ Όταν πατηθεί το πλήκτρο Ολοκλήρωση/Αποστολή/Go στο απλό πληκτρολόγιο μιας σειράς, αυτόματα να αλλαχθεί το πληκτρολόγιο.
diff --git a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-fa-rIR/strings_kp2a.xml b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-fa-rIR/strings_kp2a.xml
new file mode 100644
index 00000000..a934ff65
--- /dev/null
+++ b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-fa-rIR/strings_kp2a.xml
@@ -0,0 +1,8 @@
+
+
+
+ ورودی را انتخاب کنید
+ کاربر
+ کلمه عبور
+ پر کردن خودکار فعال شد
+
diff --git a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-gl-rES/strings_kp2a.xml b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-gl-rES/strings_kp2a.xml
new file mode 100644
index 00000000..981a248c
--- /dev/null
+++ b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-gl-rES/strings_kp2a.xml
@@ -0,0 +1,11 @@
+
+
+
+ Seleccionar outra entrada
+ Seleccionar entrada
+ Usuario
+ Contrasinal
+ Teclado simple
+ Bloquear a base de datos ao rematar
+ Cambiar de teclado ao rematar
+
diff --git a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-it/strings_kp2a.xml b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-it/strings_kp2a.xml
index 079889d7..82f82a42 100644
--- a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-it/strings_kp2a.xml
+++ b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-it/strings_kp2a.xml
@@ -6,9 +6,9 @@
Cerca la voce \"%1$s\"UtentePassword
- Impostazioni delle credenziali di ingresso
- Auto-Compilazione abilitata
- 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.
+ Impostazioni di inserimento credenziali
+ Completamento automatico abilitato
+ 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.Ricorda i testi di suggerimento del campoSe 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.Tastiera semplice
diff --git a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-iw/strings_kp2a.xml b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-iw/strings_kp2a.xml
index 1db547b9..dc007631 100644
--- a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-iw/strings_kp2a.xml
+++ b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-iw/strings_kp2a.xml
@@ -8,6 +8,7 @@
סיסמההגדרות קלט האימותהשלמה-אוטומאטית מאופשרת
+ מילוי אוטומטי בטקסט בעת הזנת שדה ריק, אם ערך Keepass2Android אינו זמין עבור לוח המקשים, יש ערך אשר תואם את טקסט הרמז של השדה.זכור את שדה טקסט הרמזיםמקלדת פשוטהנעל מסד הנתונים בסיום
diff --git a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-ja/strings_kp2a.xml b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-ja/strings_kp2a.xml
index 7dc5a795..e7f8e6d0 100644
--- a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-ja/strings_kp2a.xml
+++ b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-ja/strings_kp2a.xml
@@ -3,18 +3,18 @@
別のエントリを選択エントリの選択
- エントリを\"%1$s\"で検索します。
+ エントリを\"%1$s\"で検索ユーザーパスワード資格情報の入力の設定オートフィルを有効にする
- Keepass2Android のエントリーでキーボードが使用可能で、フィールドのヒントのテキストに一致する値がある場合、空のフィールドに自動的にテキストが入力されます。
- フィールドのヒントのテキストを保存
- Keepass2Android の値を手動で選択してテキスト フィールドを入力する場合、テキスト フィールドに入力された値を保存します。後でそのヒント テキストを使用してテキスト フィールドを検出します。
+ Keepass2Android のエントリーでキーボードが使用可能で、フィールドのヒントテキストに一致する値がある場合、空のフィールドに自動的にテキストが入力されます。
+ フィールドのヒントテキストを保存
+ Keepass2Android の値を手動で選択してテキストフィールドを入力する場合、テキストフィールドに入力された値を保存します。後でそのヒントテキストを使用してテキストフィールドを検出します。シンプルキーボード
- エントリがキーボードで利用できる場合シンプルな1行のキーボードをを表示します。もし無効にした場合、Keepass2Androidキーが押されたときにダイアログが表示されます。
- 完了時にデーターベースをロックする
- シンプルな1行のキーボードで完了/送信/実行キーを押したときにデーターベースを自動でロックします。
+ エントリーがキーボードで利用できる場合にシンプルな1行のキーボードを表示します。無効の場合、Keepass2Androidキーが押されるとダイアログが表示されます。
+ 完了時にデータベースをロックする
+ シンプルな1行のキーボードで完了/送信/実行キーを押したときにデータベースを自動でロックします。完了時にキーボードを切り替えるシンプルな1行のキーボードで完了/送信/実行キーを押したときにキーボードを切り替えます。
diff --git a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-nl/strings_kp2a.xml b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-nl/strings_kp2a.xml
index 0cf38faf..5fd01ea6 100644
--- a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-nl/strings_kp2a.xml
+++ b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-nl/strings_kp2a.xml
@@ -6,7 +6,7 @@
Zoek voor regel met \"%1$s\"GebruikerWachtwoord
- Referentie invoer instellingen
+ Instellingen voor invoer van logingegevensAutomatisch-vullen ingeschakeldVult 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.Onthoud veld hint-teksten
@@ -14,7 +14,7 @@
Eenvoudig toetsenbordToon het eenvoudige toetsenbord als een KP2A regel beschikbaar is voor het toetsenbord. Wanneer uitgeschakeld, een venster word getoond als de Keepass2Android toets is ingedrukt.Vergrendel de database na voltooiing
- Als de Gedaan/Verzenden/Gaan toets op het eenvoudige toetsenbord is ingedrukt, vergrendel automatisch de database.
+ Als de Gedaan/Verzenden/Gaan toets op het eenvoudige toetsenbord is ingedrukt, vergrendel dan automatisch de database.Wissel toetsenbord na voltooiing
- Als de Gedaan/Verzenden/Gaan toets op het eenvoudige toetsenbord is ingedrukt, verwissel het toetsenbord.
+ Als de Gedaan/Verzenden/Gaan toets op het eenvoudige toetsenbord is ingedrukt, verwissel dan het toetsenbord.
diff --git a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-ru/strings_kp2a.xml b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-ru/strings_kp2a.xml
index 9f95c31a..97f87285 100644
--- a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-ru/strings_kp2a.xml
+++ b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-ru/strings_kp2a.xml
@@ -7,7 +7,7 @@
ПользовательПарольПараметры ввода учетных данных
- Автозаполнение включено
+ АвтоЗаполнение включеноАвтоматически заполняет текст при входе в пустое поле, если запись Keepass2Android доступна для клавиатуры и есть значение, соответствующее тексту подсказки для поля.Запоминать тексты подсказки для полейПри ручном заполнении текстового поля выбором значения Keepass2Android запоминает, какое значение было введено в текстовое поле.
diff --git a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-tr/strings_kp2a.xml b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-tr/strings_kp2a.xml
index 8f47826c..121b2dad 100644
--- a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-tr/strings_kp2a.xml
+++ b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-tr/strings_kp2a.xml
@@ -1,14 +1,20 @@
- Başka bir kayıt seçin
- Kayıt seçin
- Kayıt \"%1$s\" ile arama
+ Başka bir kayıt seç
+ Kayıt seç
+ \"%1$s\" ile kayıt aramaKullanıcı
- Şifre
+ ParolaGiriş kimlik bilgisi ayarlarıOtomatik doldurma etkin
+ Klavyede Keepass2Android kaydı mevcutsa ve bu değer bir alan ipucu metniyle eşleşiyorsa, boş alan otomatik olarak doldurulur.
+ Alan ipucu metinlerini hatırla
+ 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.Basit klavye
- İşiniz bittiğinde veritabanı kilitlensin
- İşiniz bittiğinde klavyeyi değiştir
+ 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.
+ Bittiğinde veritabanını kilitle
+ 1 satırlı basit klavyede Tamam/Gönder/Git tuşlarına basıldığında, veri tabanını otomatik kilitler.
+ Bittiğinde klavyeyi değiştir
+ 1 satırlı basit klavyede Tamam/Gönder/Git tuşlarına basıldığında, klavyeyi değiştirir.
diff --git a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-uk/strings_kp2a.xml b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-uk/strings_kp2a.xml
index 27ea1536..08c0b381 100644
--- a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-uk/strings_kp2a.xml
+++ b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-uk/strings_kp2a.xml
@@ -3,18 +3,18 @@
Вибрати інший записВибрати запис
- Пошук запису з \'%1$s\'
+ Пошук запису з \"%1$s\"КористувачПарольНалаштування вводу облікових даних
- Автозаповнення увімкнуте
- Автоматично заповнює текст, коли введене порожнє поле, якщо запис Keepass2Android є доступним для клавіатури та існує значення, що відповідає тексту підказки поля.
- Запам\'ятати тексти підказки для полів
- Якщо текстове поле заповнене значенням Keepass2Android, що було вибране вручну, запам\'ятовувати, яке значення було введене в текстове поле. Наступного разу це поле буде визначене за його текстом підказки.
+ Автозаповнення увімкнено
+ Автоматично заповнює текст при вході в порожнє поле, якщо запис Keepass2Android є доступним для клавіатури та існує значення, що відповідає тексту підказки для поля.
+ Запам\'ятовувати тексти підказки для полів
+ При ручному заповненні текстового поля вибором значення, Keepass2Android запам\'ятовує, яке значення було введене в текстове поле. Надалі текстове поле буде визначатися за текстом його підказки.Проста клавіатура
- Показувати просту 1-рядну клавіатуру, якщо запис є доступним для клавіатури. Якщо вимкнено, відкривається діалог, коли натискається кнопка Keepass2Android.
- Блокувати базу даних після завершення
- При натисканні кнопки Виконано/Відправити/Перейти на простій 1-рядній клавіатурі, автоматично блокувати базу даних.
+ Показувати просту 1-рядну клавіатуру, якщо запис є доступним для клавіатури. Якщо вимкнено, діалогове вікно відкривається при натисканні кнопки Keepass2Android.
+ Блокувати базу паролів після завершення
+ При натисканні кнопки Виконано/Відправити/Перейти на простій 1-рядній клавіатурі, автоматично блокувати базу паролів.Змінити клавіатуру після завершенняПри натисканні кнопки Виконано/Відправити/Перейти на простій 1-рядній клавіатурі, змінити клавіатуру.
diff --git a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-vi/strings_kp2a.xml b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-vi/strings_kp2a.xml
index 1563927a..0e4086d9 100644
--- a/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-vi/strings_kp2a.xml
+++ b/src/java/KP2ASoftkeyboard_AS/app/src/main/res/values-vi/strings_kp2a.xml
@@ -1,5 +1,5 @@
-
+
Chọn mục khácChọn mục
diff --git a/src/java/android-filechooser-AS/.gradle/2.2.1/taskArtifacts/fileHashes.bin b/src/java/android-filechooser-AS/.gradle/2.2.1/taskArtifacts/fileHashes.bin
new file mode 100644
index 00000000..91fce0c6
Binary files /dev/null and b/src/java/android-filechooser-AS/.gradle/2.2.1/taskArtifacts/fileHashes.bin differ
diff --git a/src/java/android-filechooser-AS/.gradle/2.2.1/taskArtifacts/fileSnapshots.bin b/src/java/android-filechooser-AS/.gradle/2.2.1/taskArtifacts/fileSnapshots.bin
new file mode 100644
index 00000000..f46c2226
Binary files /dev/null and b/src/java/android-filechooser-AS/.gradle/2.2.1/taskArtifacts/fileSnapshots.bin differ
diff --git a/src/java/android-filechooser-AS/.gradle/2.2.1/taskArtifacts/outputFileStates.bin b/src/java/android-filechooser-AS/.gradle/2.2.1/taskArtifacts/outputFileStates.bin
new file mode 100644
index 00000000..3eabf872
Binary files /dev/null and b/src/java/android-filechooser-AS/.gradle/2.2.1/taskArtifacts/outputFileStates.bin differ
diff --git a/src/java/android-filechooser-AS/.gradle/2.2.1/taskArtifacts/taskArtifacts.bin b/src/java/android-filechooser-AS/.gradle/2.2.1/taskArtifacts/taskArtifacts.bin
new file mode 100644
index 00000000..28d66375
Binary files /dev/null and b/src/java/android-filechooser-AS/.gradle/2.2.1/taskArtifacts/taskArtifacts.bin differ
diff --git a/src/java/android-filechooser-AS/.idea/.name b/src/java/android-filechooser-AS/.idea/.name
new file mode 100644
index 00000000..04698307
--- /dev/null
+++ b/src/java/android-filechooser-AS/.idea/.name
@@ -0,0 +1 @@
+code
\ No newline at end of file
diff --git a/src/java/android-filechooser-AS/.idea/compiler.xml b/src/java/android-filechooser-AS/.idea/compiler.xml
new file mode 100644
index 00000000..96cc43ef
--- /dev/null
+++ b/src/java/android-filechooser-AS/.idea/compiler.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/java/android-filechooser-AS/.idea/copyright/profiles_settings.xml b/src/java/android-filechooser-AS/.idea/copyright/profiles_settings.xml
new file mode 100644
index 00000000..e7bedf33
--- /dev/null
+++ b/src/java/android-filechooser-AS/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/java/android-filechooser-AS/.idea/gradle.xml b/src/java/android-filechooser-AS/.idea/gradle.xml
new file mode 100644
index 00000000..3ed2e6cb
--- /dev/null
+++ b/src/java/android-filechooser-AS/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/java/android-filechooser-AS/.idea/libraries/support_v4_18_0_0.xml b/src/java/android-filechooser-AS/.idea/libraries/support_v4_18_0_0.xml
new file mode 100644
index 00000000..f8f83da7
--- /dev/null
+++ b/src/java/android-filechooser-AS/.idea/libraries/support_v4_18_0_0.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/java/android-filechooser-AS/.idea/misc.xml b/src/java/android-filechooser-AS/.idea/misc.xml
new file mode 100644
index 00000000..8f712fe7
--- /dev/null
+++ b/src/java/android-filechooser-AS/.idea/misc.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.7
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/java/android-filechooser-AS/.idea/modules.xml b/src/java/android-filechooser-AS/.idea/modules.xml
new file mode 100644
index 00000000..8528435a
--- /dev/null
+++ b/src/java/android-filechooser-AS/.idea/modules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/java/KP2ASoftkeyboard_AS/.idea/vcs.xml b/src/java/android-filechooser-AS/.idea/vcs.xml
similarity index 67%
rename from src/java/KP2ASoftkeyboard_AS/.idea/vcs.xml
rename to src/java/android-filechooser-AS/.idea/vcs.xml
index c2365ab1..6564d52d 100644
--- a/src/java/KP2ASoftkeyboard_AS/.idea/vcs.xml
+++ b/src/java/android-filechooser-AS/.idea/vcs.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/src/java/android-filechooser-AS/.idea/workspace.xml b/src/java/android-filechooser-AS/.idea/workspace.xml
new file mode 100644
index 00000000..93bbbdf7
--- /dev/null
+++ b/src/java/android-filechooser-AS/.idea/workspace.xml
@@ -0,0 +1,1804 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/java/android-filechooser-AS/app/app.iml b/src/java/android-filechooser-AS/app/app.iml
new file mode 100644
index 00000000..b6980838
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/app.iml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/java/android-filechooser-AS/app/build.gradle b/src/java/android-filechooser-AS/app/build.gradle
new file mode 100644
index 00000000..bf256a58
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/build.gradle
@@ -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'
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/AndroidManifest.xml b/src/java/android-filechooser-AS/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..e4ee7192
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/BaseFileAdapter.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/BaseFileAdapter.java
new file mode 100644
index 00000000..7553634a
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/BaseFileAdapter.java
@@ -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 mSelectedChildrenMap = new SparseArray();
+
+ @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.
+ *
+ * Note: This will not 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.
+ *
+ * Note: 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.
+ *
+ * Note: This will not 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.
+ *
+ * Note: 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 getSelectedItems() {
+ ArrayList res = new ArrayList();
+
+ 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.
+ *
+ * Note: 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.
+ *
+ * Note: 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 declared 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(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
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/FileChooserActivity.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/FileChooserActivity.java
new file mode 100644
index 00000000..b5b06ecd
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/FileChooserActivity.java
@@ -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.
+ *
+ *
Notes:
+ *
+ *
+ *
About keys {@link FileChooserActivity#EXTRA_ROOTPATH},
+ * {@link FileChooserActivity#EXTRA_SELECT_FILE} and preference
+ * {@link DisplayPrefs#isRememberLastLocation(Context)}, the priorities of them
+ * are:
+ *
+ *
+ *
+ *
+ * @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.
+ *
+ * If {@link LocalFileProvider} is used, then default is SD card, if SD card
+ * is not available, {@code "/"} will be used.
+ *
+ * Note: The value of this key is a file provider's {@link Uri}. For
+ * example with {@link LocalFileProvider}, you can use this command:
+ *
+ *
+ */
+ public static final String EXTRA_ROOTPATH = CLASSNAME + ".rootpath";
+
+ /**
+ * Key to hold the authority of file provider.
+ *
+ * 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}.
+ *
+ * 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 (not
+ * 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 (not
+ * 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:
+ *
+ *
+ *
+ */
+ private void setupFooter() {
+ /*
+ * By default, view group footer and all its child views are hidden.
+ */
+
+ ViewGroup viewGroupFooterContainer = (ViewGroup) getView()
+ .findViewById(R.id.afc_viewgroup_footer_container);
+ ViewGroup viewGroupFooter = (ViewGroup) getView().findViewById(
+ R.id.afc_viewgroup_footer);
+
+ if (mIsSaveDialog) {
+ viewGroupFooterContainer.setVisibility(View.VISIBLE);
+ viewGroupFooter.setVisibility(View.VISIBLE);
+
+ mTextSaveas.setVisibility(View.VISIBLE);
+ mTextSaveas.setText(getArguments().getString(
+ FileChooserActivity.EXTRA_DEFAULT_FILENAME));
+ mTextSaveas
+ .setOnEditorActionListener(new TextView.OnEditorActionListener() {
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId,
+ KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ Ui.showSoftKeyboard(v, false);
+ mBtnOk.performClick();
+ return true;
+ }
+ return false;
+ }// onEditorAction()
+ });
+ mTextSaveas.addTextChangedListener(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) {
+ /*
+ * Do nothing.
+ */
+ }// beforeTextChanged()
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ /*
+ * If the user taps a file, the tag is set to that file's
+ * URI. But if the user types the file name, we remove the
+ * tag.
+ */
+ mTextSaveas.setTag(null);
+ }// afterTextChanged()
+ });
+
+ mBtnOk.setVisibility(View.VISIBLE);
+ mBtnOk.setOnClickListener(mBtnOk_SaveDialog_OnClickListener);
+ mBtnOk.setBackgroundResource(Ui.resolveAttribute(getActivity(),
+ R.attr.afc_selector_button_ok_saveas));
+
+ int size = getResources().getDimensionPixelSize(
+ R.dimen.afc_button_ok_saveas_size);
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mBtnOk
+ .getLayoutParams();
+ lp.width = size;
+ lp.height = size;
+ mBtnOk.setLayoutParams(lp);
+ }// this is in save mode
+ else {
+ if (mIsMultiSelection) {
+ viewGroupFooterContainer.setVisibility(View.VISIBLE);
+ viewGroupFooter.setVisibility(View.VISIBLE);
+
+ ViewGroup.LayoutParams lp = viewGroupFooter.getLayoutParams();
+ lp.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+ viewGroupFooter.setLayoutParams(lp);
+
+ mBtnOk.setMinWidth(getResources().getDimensionPixelSize(
+ R.dimen.afc_single_button_min_width));
+ mBtnOk.setText(android.R.string.ok);
+ mBtnOk.setVisibility(View.VISIBLE);
+ mBtnOk.setOnClickListener(mBtnOk_OpenDialog_OnClickListener);
+ }
+ }// this is in open mode
+ }// setupFooter()
+
+ /**
+ * Shows footer view.
+ *
+ * @param show
+ * {@code true} or {@code false}.
+ * @param text
+ * the message you want to set.
+ * @param center
+ * {@code true} or {@code false}.
+ */
+ @SuppressLint("InlinedApi")
+ private void showFooterView(boolean show, String text, boolean center) {
+ if (show) {
+ mFooterView.setText(text);
+
+ RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.MATCH_PARENT,
+ RelativeLayout.LayoutParams.MATCH_PARENT);
+ if (!center)
+ lp.addRule(RelativeLayout.ABOVE,
+ R.id.afc_view_files_footer_view);
+ mViewFilesContainer.setLayoutParams(lp);
+
+ lp = (RelativeLayout.LayoutParams) mFooterView.getLayoutParams();
+ lp.addRule(RelativeLayout.CENTER_IN_PARENT, center ? 1 : 0);
+ lp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, center ? 0 : 1);
+ mFooterView.setLayoutParams(lp);
+
+ mFooterView.setVisibility(View.VISIBLE);
+ } else
+ mFooterView.setVisibility(View.GONE);
+ }// showFooterView()
+
+ /**
+ * This should be called after the owner activity has been created
+ * successfully.
+ */
+ private void initGestureDetector() {
+ mListviewFilesGestureDetector = new GestureDetector(getActivity(),
+ new GestureDetector.SimpleOnGestureListener() {
+
+ private Object getData(float x, float y) {
+ int i = getSubViewId(x, y);
+ if (i >= 0)
+ return mViewFiles.getItemAtPosition(mViewFiles
+ .getFirstVisiblePosition() + i);
+ return null;
+ }// getSubView()
+
+ private int getSubViewId(float x, float y) {
+ Rect r = new Rect();
+ for (int i = 0; i < mViewFiles.getChildCount(); i++) {
+ mViewFiles.getChildAt(i).getHitRect(r);
+ if (r.contains((int) x, (int) y))
+ return i;
+ }
+
+ return -1;
+ }// getSubViewId()
+
+ /**
+ * Gets {@link Cursor} from {@code e}.
+ *
+ * @param e
+ * {@link MotionEvent}.
+ * @return the cursor, or {@code null} if not available.
+ */
+ private Cursor getData(MotionEvent e) {
+ Object o = getData(e.getX(), e.getY());
+ return o instanceof Cursor ? (Cursor) o : null;
+ }// getDataModel()
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ /*
+ * Do nothing.
+ */
+ }// onLongPress()
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ /*
+ * Do nothing.
+ */
+ return false;
+ }// onSingleTapConfirmed()
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ if (mDoubleTapToChooseFiles) {
+ if (mIsMultiSelection)
+ return false;
+
+ Cursor cursor = getData(e);
+ if (cursor == null)
+ return false;
+
+ if (BaseFileProviderUtils.isDirectory(cursor)
+ && BaseFile.FILTER_FILES_ONLY == mFilterMode)
+ return false;
+
+ /*
+ * If mFilterMode == FILTER_DIRECTORIES_ONLY, files
+ * won't be shown.
+ */
+
+ if (mIsSaveDialog) {
+ if (BaseFileProviderUtils.isFile(cursor)) {
+ mTextSaveas.setText(BaseFileProviderUtils
+ .getFileName(cursor));
+ /*
+ * Always set tag after setting text, or tag
+ * will be reset to null.
+ */
+ mTextSaveas.setTag(BaseFileProviderUtils
+ .getUri(cursor));
+ checkSaveasFilenameAndFinish();
+ } else
+ return false;
+ } else
+ finish(BaseFileProviderUtils.getUri(cursor));
+ }// double tap to choose files
+ else {
+ /*
+ * Do nothing.
+ */
+ return false;
+ }// single tap to choose files
+
+ return true;
+ }// onDoubleTap()
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2,
+ float velocityX, float velocityY) {
+ /*
+ * Sometimes e1 or e2 can be null. This came from users'
+ * experiences.
+ */
+ if (e1 == null || e2 == null)
+ return false;
+
+ 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) {
+ int pos = getSubViewId(e1.getX(), e1.getY());
+ if (pos >= 0) {
+ /*
+ * Don't let this event to be recognized as a
+ * single tap.
+ */
+ MotionEvent cancelEvent = MotionEvent
+ .obtain(e1);
+ cancelEvent
+ .setAction(MotionEvent.ACTION_CANCEL);
+ mViewFiles.onTouchEvent(cancelEvent);
+
+ deleteFile(mViewFiles.getFirstVisiblePosition()
+ + pos);
+ }
+ }
+
+ /*
+ * Always return false to let the default handler draw
+ * the item properly.
+ */
+ return false;
+ }// onFling()
+ });// mListviewFilesGestureDetector
+ }// initGestureDetector()
+
+ /**
+ * Connects to file provider service, then loads root directory. If can not,
+ * then finishes this activity with result code =
+ * {@link Activity#RESULT_CANCELED}
+ *
+ * @param savedInstanceState
+ */
+ private void loadInitialPath(final Bundle savedInstanceState) {
+ if (BuildConfig.DEBUG)
+ Log.d(CLASSNAME, String.format(
+ "loadInitialPath() >> authority=[%s] | mRoot=[%s]",
+ mFileProviderAuthority, mRoot));
+
+ /*
+ * Priorities for starting path:
+ *
+ * 1. Current location (in case the activity has been killed after
+ * configurations changed).
+ *
+ * 2. Selected file from key EXTRA_SELECT_FILE.
+ *
+ * 3. Root path from key EXTRA_ROOTPATH.
+ *
+ * 4. Last location.
+ */
+
+ new LoadingDialog(getActivity(), false) {
+
+ /**
+ * In onPostExecute(), if result is null then check this value. If
+ * this is not null, show a toast and finish. If this is null, call
+ * showCannotConnectToServiceAndWaitForTheUserToFinish().
+ */
+ String errMsg = null;
+
+ @Override
+ protected Bundle doInBackground(Void... params) {
+ /*
+ * Current location
+ */
+ Uri path = (Uri) (savedInstanceState != null ? savedInstanceState
+ .getParcelable(CURRENT_LOCATION) : null);
+
+ /*
+ * Selected file
+ */
+ if (path == null) {
+ path = (Uri) getArguments().getParcelable(
+ FileChooserActivity.EXTRA_SELECT_FILE);
+ if (path != null
+ && BaseFileProviderUtils.fileExists(getActivity(),
+ path))
+ path = BaseFileProviderUtils.getParentFile(
+ getActivity(), path);
+ }
+
+ /*
+ * Rootpath
+ */
+ if (path == null
+ || !BaseFileProviderUtils.isDirectory(getActivity(),
+ path)) {
+ path = mRoot;
+ }
+
+ /*
+ * Last location
+ */
+ if (path == null
+ && DisplayPrefs.isRememberLastLocation(getActivity())) {
+ String lastLocation = DisplayPrefs
+ .getLastLocation(getActivity());
+ if (lastLocation != null)
+ path = Uri.parse(lastLocation);
+ }
+
+ if (path == null
+ || !BaseFileProviderUtils.isDirectory(getActivity(),
+ path))
+ path = BaseFileProviderUtils.getDefaultPath(
+ getActivity(),
+ path == null ? mFileProviderAuthority : path
+ .getAuthority());
+
+ if (path == null)
+ return null;
+
+ if (BuildConfig.DEBUG)
+ Log.d(CLASSNAME, "loadInitialPath() >> " + path);
+
+ publishProgress(path);
+
+ if (BaseFileProviderUtils.fileCanRead(getActivity(), path)) {
+ Bundle result = new Bundle();
+ result.putParcelable(PATH, path);
+ return result;
+ } else {
+ errMsg = getString(R.string.afc_pmsg_cannot_access_dir,
+ BaseFileProviderUtils.getFileName(getActivity(),
+ path));
+ }
+
+ return null;
+ }// doInBackground()
+
+ @Override
+ protected void onProgressUpdate(Uri... progress) {
+ setCurrentLocation(progress[0]);
+ }// onProgressUpdate()
+
+ @Override
+ protected void onPostExecute(Bundle result) {
+ super.onPostExecute(result);
+
+ if (result != null) {
+ /*
+ * Prepare the loader. Either re-connect with an existing
+ * one, or start a new one.
+ */
+ getLoaderManager().initLoader(mIdLoaderData, result,
+ FragmentFiles.this);
+ } else if (errMsg != null) {
+ Dlg.toast(getActivity(), errMsg, Dlg.LENGTH_SHORT);
+ getActivity().finish();
+ } else
+ showCannotConnectToServiceAndWaitForTheUserToFinish();
+ }// onPostExecute()
+
+ }.execute();
+ }// loadInitialPath()
+
+ /**
+ * Checks if the fragment is loading files...
+ *
+ * @return {@code true} or {@code false}.
+ */
+ public boolean isLoading() {
+ return mLoading;
+ }// isLoading()
+
+ /**
+ * Cancels the loader in progress.
+ */
+ public void cancelPreviousLoader() {
+ /*
+ * Adds a fake path...
+ */
+ if (getCurrentLocation() != null
+ && getLoaderManager().getLoader(mIdLoaderData) != null)
+ BaseFileProviderUtils.cancelTask(getActivity(),
+ getCurrentLocation().getAuthority(), mIdLoaderData);
+
+ mLoading = false;
+ }// cancelPreviousLoader()
+
+ /**
+ * As the name means...
+ */
+ private void showCannotConnectToServiceAndWaitForTheUserToFinish() {
+ Dlg.showError(getActivity(),
+ R.string.afc_msg_cannot_connect_to_file_provider_service,
+ new DialogInterface.OnCancelListener() {
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ getActivity().setResult(Activity.RESULT_CANCELED);
+ getActivity().finish();
+ }// onCancel()
+ });
+ }// showCannotConnectToServiceAndWaitForTheUserToFinish()
+
+ /**
+ * Gets last location.
+ *
+ * @return the last location.
+ */
+ private Uri getLastLocation() {
+ return mLastLocation;
+ }// getLastLocation()
+
+ /**
+ * Gets current location.
+ *
+ * @return the current location.
+ */
+ private Uri getCurrentLocation() {
+ return mCurrentLocation;
+ }// getCurrentLocation()
+
+ /**
+ * Sets current location.
+ *
+ * @param location
+ * the location to set.
+ */
+ private void setCurrentLocation(Uri location) {
+ /*
+ * Do this so history's listener will retrieve the right current
+ * location.
+ */
+ mLastLocation = mCurrentLocation;
+ mCurrentLocation = location;
+
+ if (mHistory.indexOf(location) < 0) {
+ mHistory.truncateAfter(mLastLocation);
+ mHistory.push(location);
+ } else
+ mHistory.notifyHistoryChanged();
+
+ updateDbHistory(location);
+ }// setCurrentLocation()
+
+ private void goHome() {
+ goTo(mRoot);
+ }// goHome()
+
+
+ private static final int[] BUTTON_SORT_IDS = {
+ R.id.afc_button_sort_by_name_asc,
+ R.id.afc_button_sort_by_name_desc,
+ R.id.afc_button_sort_by_size_asc,
+ R.id.afc_button_sort_by_size_desc,
+ R.id.afc_button_sort_by_date_asc, R.id.afc_button_sort_by_date_desc };
+
+ /**
+ * Show a dialog for sorting options and resort file list after user
+ * selected an option.
+ */
+ private void resortViewFiles() {
+ final Dialog dialog = new Dialog(getActivity(), Ui.resolveAttribute(
+ getActivity(), R.attr.afc_theme_dialog));
+ dialog.setCanceledOnTouchOutside(true);
+
+ // get the index of button of current sort type
+ int btnCurrentSortTypeIdx = 0;
+ switch (DisplayPrefs.getSortType(getActivity())) {
+ case BaseFile.SORT_BY_NAME:
+ btnCurrentSortTypeIdx = 0;
+ break;
+ case BaseFile.SORT_BY_SIZE:
+ btnCurrentSortTypeIdx = 2;
+ break;
+ case BaseFile.SORT_BY_MODIFICATION_TIME:
+ btnCurrentSortTypeIdx = 4;
+ break;
+ }
+ if (!DisplayPrefs.isSortAscending(getActivity()))
+ btnCurrentSortTypeIdx++;
+
+ View.OnClickListener listener = new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ dialog.dismiss();
+
+ if (v.getId() == R.id.afc_button_sort_by_name_asc) {
+ DisplayPrefs.setSortType(getActivity(),
+ BaseFile.SORT_BY_NAME);
+ DisplayPrefs.setSortAscending(getActivity(), true);
+ } else if (v.getId() == R.id.afc_button_sort_by_name_desc) {
+ DisplayPrefs.setSortType(getActivity(),
+ BaseFile.SORT_BY_NAME);
+ DisplayPrefs.setSortAscending(getActivity(), false);
+ } else if (v.getId() == R.id.afc_button_sort_by_size_asc) {
+ DisplayPrefs.setSortType(getActivity(),
+ BaseFile.SORT_BY_SIZE);
+ DisplayPrefs.setSortAscending(getActivity(), true);
+ } else if (v.getId() == R.id.afc_button_sort_by_size_desc) {
+ DisplayPrefs.setSortType(getActivity(),
+ BaseFile.SORT_BY_SIZE);
+ DisplayPrefs.setSortAscending(getActivity(), false);
+ } else if (v.getId() == R.id.afc_button_sort_by_date_asc) {
+ DisplayPrefs.setSortType(getActivity(),
+ BaseFile.SORT_BY_MODIFICATION_TIME);
+ DisplayPrefs.setSortAscending(getActivity(), true);
+ } else if (v.getId() == R.id.afc_button_sort_by_date_desc) {
+ DisplayPrefs.setSortType(getActivity(),
+ BaseFile.SORT_BY_MODIFICATION_TIME);
+ DisplayPrefs.setSortAscending(getActivity(), false);
+ }
+
+ /*
+ * Reload current location.
+ */
+ goTo(getCurrentLocation());
+ getActivity().supportInvalidateOptionsMenu();
+ }// onClick()
+ };// listener
+
+ View view = getLayoutInflater(null).inflate(
+ R.layout.afc_settings_sort_view, null);
+ for (int i = 0; i < BUTTON_SORT_IDS.length; i++) {
+ View v = view.findViewById(BUTTON_SORT_IDS[i]);
+ v.setOnClickListener(listener);
+ if (i == btnCurrentSortTypeIdx) {
+ v.setEnabled(false);
+ if (v instanceof Button)
+ ((Button) v).setText(R.string.afc_bullet);
+ }
+ }
+
+ dialog.setTitle(R.string.afc_title_sort_by);
+ dialog.setContentView(view);
+ dialog.show();
+ }// resortViewFiles()
+
+ /**
+ * Switch view type between {@link ViewType#LIST} and {@link ViewType#GRID}
+ */
+ private void switchViewType() {
+ switch (DisplayPrefs.getViewType(getActivity())) {
+ case GRID:
+ DisplayPrefs.setViewType(getActivity(), ViewType.LIST);
+ break;
+ case LIST:
+ DisplayPrefs.setViewType(getActivity(), ViewType.GRID);
+ break;
+ }
+
+ setupViewFiles();
+ getActivity().supportInvalidateOptionsMenu();
+ goTo(getCurrentLocation());
+ }// switchViewType()
+
+ /**
+ * Checks current conditions to see if we can create new directory. Then
+ * confirms user to do so.
+ */
+ private void checkConditionsThenConfirmUserToCreateNewDir() {
+ if (LocalFileContract.getAuthority(getActivity()).equals(
+ mFileProviderAuthority)
+ && !Utils.hasPermissions(getActivity(),
+ Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ Dlg.toast(
+ getActivity(),
+ R.string.afc_msg_app_doesnot_have_permission_to_create_files,
+ Dlg.LENGTH_SHORT);
+ return;
+ }
+
+ new LoadingDialog(getActivity(), false) {
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return getCurrentLocation() != null
+ && BaseFileProviderUtils.fileCanWrite(getActivity(),
+ getCurrentLocation());
+ }// doInBackground()
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ super.onPostExecute(result);
+
+ if (result)
+ showNewDirectoryCreationDialog();
+ else
+ Dlg.toast(getActivity(),
+ R.string.afc_msg_cannot_create_new_folder_here,
+ Dlg.LENGTH_SHORT);
+ }// onProgressUpdate()
+
+ }.execute();
+ }// checkConditionsThenConfirmUserToCreateNewDir()
+
+ /**
+ * Confirms user to create new directory.
+ */
+ private void showNewDirectoryCreationDialog() {
+ final AlertDialog dialog = Dlg.newAlertDlg(getActivity());
+
+ View view = getLayoutInflater(null).inflate(
+ R.layout.afc_simple_text_input_view, null);
+ final EditText textFile = (EditText) view.findViewById(R.id.afc_text1);
+ textFile.setHint(R.string.afc_hint_folder_name);
+ textFile.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId,
+ KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ Ui.showSoftKeyboard(v, false);
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE)
+ .performClick();
+ return true;
+ }
+ return false;
+ }
+ });
+
+ dialog.setView(view);
+ dialog.setTitle(R.string.afc_cmd_new_folder);
+ dialog.setIcon(android.R.drawable.ic_menu_add);
+ dialog.setButton(DialogInterface.BUTTON_POSITIVE,
+ getString(android.R.string.ok),
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final String name = textFile.getText().toString()
+ .trim();
+ if (!FileUtils.isFilenameValid(name)) {
+ Dlg.toast(
+ getActivity(),
+ getString(
+ R.string.afc_pmsg_filename_is_invalid,
+ name), Dlg.LENGTH_SHORT);
+ return;
+ }
+
+ new LoadingDialog(getActivity(), false) {
+
+ @Override
+ protected Uri doInBackground(Void... params) {
+ return getActivity()
+ .getContentResolver()
+ .insert(BaseFile
+ .genContentUriBase(
+ getCurrentLocation()
+ .getAuthority())
+ .buildUpon()
+ .appendPath(
+ getCurrentLocation()
+ .getLastPathSegment())
+ .appendQueryParameter(
+ BaseFile.PARAM_NAME,
+ name)
+ .appendQueryParameter(
+ BaseFile.PARAM_FILE_TYPE,
+ Integer.toString(BaseFile.FILE_TYPE_DIRECTORY))
+ .build(), null);
+ }// doInBackground()
+
+ @Override
+ protected void onPostExecute(Uri result) {
+ super.onPostExecute(result);
+
+ if (result != null) {
+ Dlg.toast(getActivity(),
+ getString(R.string.afc_msg_done),
+ Dlg.LENGTH_SHORT);
+ } else
+ Dlg.toast(
+ getActivity(),
+ getString(
+ R.string.afc_pmsg_cannot_create_folder,
+ name), Dlg.LENGTH_SHORT);
+ }// onPostExecute()
+
+ }.execute();
+ }// onClick()
+ });
+ dialog.show();
+ Ui.showSoftKeyboard(textFile, true);
+
+ final Button buttonOk = dialog
+ .getButton(DialogInterface.BUTTON_POSITIVE);
+ buttonOk.setEnabled(false);
+
+ textFile.addTextChangedListener(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) {
+ /*
+ * Do nothing.
+ */
+ }// beforeTextChanged()
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ buttonOk.setEnabled(FileUtils.isFilenameValid(s.toString()
+ .trim()));
+ }// afterTextChanged()
+ });
+ }// showNewDirectoryCreationDialog()
+
+ /**
+ * Deletes a file.
+ *
+ * @param position
+ * the position of item to be delete.
+ */
+ private void deleteFile(final int position) {
+ Cursor cursor = (Cursor) mFileAdapter.getItem(position);
+
+ /*
+ * The cursor can be changed if the list view is updated, so we take its
+ * properties here.
+ */
+ final boolean isFile = BaseFileProviderUtils.isFile(cursor);
+ final String filename = BaseFileProviderUtils.getFileName(cursor);
+
+ if (!BaseFileProviderUtils.fileCanWrite(cursor)) {
+ Dlg.toast(
+ getActivity(),
+ getString(R.string.afc_pmsg_cannot_delete_file,
+ isFile ? getString(R.string.afc_file)
+ : getString(R.string.afc_folder), filename),
+ Dlg.LENGTH_SHORT);
+ return;
+ }
+
+ if (LocalFileContract.getAuthority(getActivity()).equals(
+ mFileProviderAuthority)
+ && !Utils.hasPermissions(getActivity(),
+ Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ Dlg.toast(
+ getActivity(),
+ R.string.afc_msg_app_doesnot_have_permission_to_delete_files,
+ Dlg.LENGTH_SHORT);
+ return;
+ }
+
+ /*
+ * The cursor can be changed if the list view is updated, so we take its
+ * properties here.
+ */
+ final int id = cursor.getInt(cursor.getColumnIndex(BaseFile._ID));
+ final Uri uri = BaseFileProviderUtils.getUri(cursor);
+
+ mFileAdapter.markItemAsDeleted(id, true);
+
+ Dlg.confirmYesno(
+ getActivity(),
+ getString(R.string.afc_pmsg_confirm_delete_file,
+ isFile ? getString(R.string.afc_file)
+ : getString(R.string.afc_folder), filename),
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingDialog(
+ getActivity(),
+ getString(
+ R.string.afc_pmsg_deleting_file,
+ isFile ? getString(R.string.afc_file)
+ : getString(R.string.afc_folder),
+ filename), true) {
+
+ final int taskId = EnvUtils.genId();
+
+ private void notifyFileDeleted() {
+ Dlg.toast(
+ getActivity(),
+ getString(
+ R.string.afc_pmsg_file_has_been_deleted,
+ isFile ? getString(R.string.afc_file)
+ : getString(R.string.afc_folder),
+ filename), Dlg.LENGTH_SHORT);
+ }// notifyFileDeleted()
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ getActivity()
+ .getContentResolver()
+ .delete(uri
+ .buildUpon()
+ .appendQueryParameter(
+ BaseFile.PARAM_TASK_ID,
+ Integer.toString(taskId))
+ .build(), null, null);
+
+ return !BaseFileProviderUtils.fileExists(
+ getActivity(), uri);
+ }// doInBackground()
+
+ @Override
+ protected void onCancelled() {
+ if (getCurrentLocation() != null)
+ BaseFileProviderUtils.cancelTask(
+ getActivity(), getCurrentLocation()
+ .getAuthority(), taskId);
+
+ new LoadingDialog(
+ getActivity(), false) {
+
+ @Override
+ protected Boolean doInBackground(
+ Void... params) {
+ return BaseFileProviderUtils
+ .fileExists(getActivity(), uri);
+ }// doInBackground()
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ super.onPostExecute(result);
+
+ if (result) {
+ mFileAdapter.markItemAsDeleted(id,
+ false);
+ Dlg.toast(getActivity(),
+ R.string.afc_msg_cancelled,
+ Dlg.LENGTH_SHORT);
+ } else
+ notifyFileDeleted();
+ }// onPostExecute()
+
+ }.execute();
+
+ super.onCancelled();
+ }// onCancelled()
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ super.onPostExecute(result);
+
+ if (result) {
+ notifyFileDeleted();
+ } else {
+ mFileAdapter.markItemAsDeleted(id, false);
+ Dlg.toast(
+ getActivity(),
+ getString(
+ R.string.afc_pmsg_cannot_delete_file,
+ isFile ? getString(R.string.afc_file)
+ : getString(R.string.afc_folder),
+ filename), Dlg.LENGTH_SHORT);
+ }
+ }// onPostExecute()
+
+ }.execute();// LoadingDialog
+ }// onClick()
+ }, new DialogInterface.OnCancelListener() {
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ mFileAdapter.markItemAsDeleted(id, false);
+ }// onCancel()
+ });
+ }// deleteFile()
+
+ /**
+ * As the name means.
+ */
+ private void checkSaveasFilenameAndFinish() {
+ new LoadingDialog(getActivity(), false) {
+
+ String filename;
+ Uri fileUri;
+ int fileType;
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+
+ /*
+ * If the user tapped a file, its URI was stored here. If not,
+ * this is null.
+ */
+ fileUri = (Uri) mTextSaveas.getTag();
+
+ /*
+ * File name and extension.
+ */
+ filename = mTextSaveas.getText().toString().trim();
+ if (fileUri == null
+ && getArguments().containsKey(
+ FileChooserActivity.EXTRA_DEFAULT_FILE_EXT)) {
+ if (!TextUtils.isEmpty(filename)) {
+ String ext = getArguments().getString(
+ FileChooserActivity.EXTRA_DEFAULT_FILE_EXT);
+ if (!filename.matches("(?si)^.+"
+ + Pattern.quote(Texts.C_PERIOD + ext) + "$")) {
+ filename += Texts.C_PERIOD + ext;
+ mTextSaveas.setText(filename);
+ }
+ }
+ }
+ }// onPreExecute()
+
+ @Override
+ protected Uri doInBackground(Void... params) {
+ if (!BaseFileProviderUtils.fileCanWrite(getActivity(),
+ getCurrentLocation())) {
+ publishProgress(getString(R.string.afc_msg_cannot_save_a_file_here));
+ return null;
+ }
+
+ if (fileUri == null && !FileUtils.isFilenameValid(filename)) {
+ publishProgress(getString(
+ R.string.afc_pmsg_filename_is_invalid, filename));
+ return null;
+ }
+
+ if (fileUri == null)
+ fileUri = getCurrentLocation()
+ .buildUpon()
+ .appendQueryParameter(BaseFile.PARAM_APPEND_NAME,
+ filename).build();
+ final Cursor cursor = getActivity().getContentResolver().query(
+ fileUri, null, null, null, null);
+ try {
+ if (cursor == null || !cursor.moveToFirst())
+ return null;
+
+ fileType = cursor.getInt(cursor
+ .getColumnIndex(BaseFile.COLUMN_TYPE));
+ return BaseFileProviderUtils.getUri(cursor);
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ }// doInBackground()
+
+ @Override
+ protected void onProgressUpdate(String... progress) {
+ Dlg.toast(getActivity(), progress[0], Dlg.LENGTH_SHORT);
+ }// onProgressUpdate()
+
+ @Override
+ protected void onPostExecute(final Uri result) {
+ super.onPostExecute(result);
+
+ if (result == null) {
+ /*
+ * TODO ?
+ */
+ return;
+ }
+
+ switch (fileType) {
+ case BaseFile.FILE_TYPE_DIRECTORY: {
+ Dlg.toast(
+ getActivity(),
+ getString(R.string.afc_pmsg_filename_is_directory,
+ filename), Dlg.LENGTH_SHORT);
+ break;
+ }// FILE_TYPE_DIRECTORY
+
+ case BaseFile.FILE_TYPE_FILE: {
+ Dlg.confirmYesno(
+ getActivity(),
+ getString(R.string.afc_pmsg_confirm_replace_file,
+ filename),
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ finish(result, true);
+ }// onClick()
+ });
+
+ break;
+ }// FILE_TYPE_FILE
+
+ case BaseFile.FILE_TYPE_NOT_EXISTED: {
+ finish(result, false);
+ break;
+ }// FILE_TYPE_NOT_EXISTED
+ }
+ }// onPostExecute()
+
+ }.execute();
+ }// checkSaveasFilenameAndFinish()
+
+ /**
+ * Goes to a specified location.
+ *
+ * @param dir
+ * a directory, of course.
+ * @since v4.3 beta
+ */
+ private void goTo(Uri dir) {
+ new LoadingDialog(getActivity(), false) {
+
+ /**
+ * In onPostExecute(), if result is null then check this value. If
+ * this is not null, show a toast. If this is null, call
+ * showCannotConnectToServiceAndWaitForTheUserToFinish().
+ */
+ String errMsg = null;
+
+ @Override
+ protected Bundle doInBackground(Uri... params) {
+ if (params[0] == null)
+ params[0] = BaseFileProviderUtils.getDefaultPath(
+ getActivity(), mFileProviderAuthority);
+ if (params[0] == null)
+ return null;
+
+ /*
+ * Check if the path of `params[0]` is same as current location,
+ * then set `params[0]` to current location. This avoids of
+ * pushing two same paths into history, because we compare the
+ * pointers (not the paths) when pushing it to history.
+ */
+ if (params[0].equals(getCurrentLocation()))
+ params[0] = getCurrentLocation();
+
+ if (BaseFileProviderUtils.fileCanRead(getActivity(), params[0])) {
+ /*
+ * Cancel previous loader if there is one.
+ */
+ cancelPreviousLoader();
+
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(PATH, params[0]);
+ return bundle;
+ }// if
+
+ errMsg = getString(R.string.afc_pmsg_cannot_access_dir,
+ BaseFileProviderUtils.getFileName(getActivity(),
+ params[0]));
+
+ return null;
+ }// doInBackground()
+
+ @Override
+ protected void onPostExecute(Bundle result) {
+ super.onPostExecute(result);
+
+ if (result != null) {
+ setCurrentLocation((Uri) result.getParcelable(PATH));
+ getLoaderManager().restartLoader(mIdLoaderData, result,
+ FragmentFiles.this);
+ } else if (errMsg != null)
+ Dlg.toast(getActivity(), errMsg, Dlg.LENGTH_SHORT);
+ else
+ showCannotConnectToServiceAndWaitForTheUserToFinish();
+ }// onPostExecute()
+
+ }.execute(dir);
+ }// goTo()
+
+ /**
+ * Updates or inserts {@code path} into history database.
+ */
+ private void updateDbHistory(Uri path) {
+ if (BuildConfig.DEBUG)
+ Log.d(CLASSNAME, "updateDbHistory() >> path = " + path);
+
+ Calendar cal = Calendar.getInstance();
+ final long beginTodayMillis = cal.getTimeInMillis()
+ - (cal.get(Calendar.HOUR_OF_DAY) * 60 * 60 * 1000
+ + cal.get(Calendar.MINUTE) * 60 * 1000 + cal
+ .get(Calendar.SECOND) * 1000);
+ if (BuildConfig.DEBUG) {
+ Log.d(CLASSNAME,
+ String.format("beginToday = %s (%s)", DbUtils
+ .formatNumber(beginTodayMillis), new Date(
+ beginTodayMillis)));
+ Log.d(CLASSNAME, String.format("endToday = %s (%s)", DbUtils
+ .formatNumber(beginTodayMillis + DateUtils.DAY_IN_MILLIS),
+ new Date(beginTodayMillis + DateUtils.DAY_IN_MILLIS)));
+ }
+
+ /*
+ * Does the update and returns the number of rows updated.
+ */
+ long time = new Date().getTime();
+ ContentValues values = new ContentValues();
+ values.put(HistoryContract.COLUMN_PROVIDER_ID,
+ BaseFileProviderUtils.getProviderId(path.getAuthority()));
+ values.put(HistoryContract.COLUMN_FILE_TYPE,
+ BaseFile.FILE_TYPE_DIRECTORY);
+ values.put(HistoryContract.COLUMN_URI, path.toString());
+ values.put(HistoryContract.COLUMN_MODIFICATION_TIME,
+ DbUtils.formatNumber(time));
+
+ int count = getActivity()
+ .getContentResolver()
+ .update(HistoryContract.genContentUri(getActivity()),
+ values,
+ String.format(
+ "%s >= '%s' and %s < '%s' and %s = %s and %s like %s",
+ HistoryContract.COLUMN_MODIFICATION_TIME,
+ DbUtils.formatNumber(beginTodayMillis),
+ HistoryContract.COLUMN_MODIFICATION_TIME,
+ DbUtils.formatNumber(beginTodayMillis
+ + DateUtils.DAY_IN_MILLIS),
+ HistoryContract.COLUMN_PROVIDER_ID,
+ DatabaseUtils.sqlEscapeString(values
+ .getAsString(HistoryContract.COLUMN_PROVIDER_ID)),
+ HistoryContract.COLUMN_URI,
+ DatabaseUtils.sqlEscapeString(values
+ .getAsString(HistoryContract.COLUMN_URI))),
+ null);
+ if (count <= 0) {
+ values.put(HistoryContract.COLUMN_CREATE_TIME,
+ DbUtils.formatNumber(time));
+ getActivity().getContentResolver().insert(
+ HistoryContract.genContentUri(getActivity()), values);
+ }
+ }// updateDbHistory()
+
+ /**
+ * As the name means.
+ */
+ private void buildAddressBar(final Uri path) {
+ if (path == null)
+ return;
+
+ mViewAddressBar.removeAllViews();
+
+ new LoadingDialog(getActivity(), false) {
+
+ LinearLayout.LayoutParams lpBtnLoc;
+ LinearLayout.LayoutParams lpDivider;
+ LayoutInflater inflater = getLayoutInflater(null);
+ final int dim = getResources().getDimensionPixelSize(
+ R.dimen.afc_5dp);
+ int count = 0;
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+
+ lpBtnLoc = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ lpBtnLoc.gravity = Gravity.CENTER;
+ }// onPreExecute()
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ Cursor cursor = getActivity().getContentResolver().query(path,
+ null, null, null, null);
+ while (cursor != null) {
+ if (cursor.moveToFirst()) {
+ publishProgress(cursor);
+ cursor.close();
+ } else
+ break;
+
+ /*
+ * Process the parent directory.
+ */
+ Uri uri = Uri.parse(cursor.getString(cursor
+ .getColumnIndex(BaseFile.COLUMN_URI)));
+ cursor = getActivity().getContentResolver().query(
+ BaseFile.genContentUriApi(uri.getAuthority())
+ .buildUpon()
+ .appendPath(BaseFile.CMD_GET_PARENT)
+ .appendQueryParameter(
+ BaseFile.PARAM_SOURCE,
+ uri.getLastPathSegment()).build(),
+ null, null, null, null);
+ }// while
+
+ return null;
+ }// doInBackground()
+
+ @Override
+ protected void onProgressUpdate(Cursor... progress) {
+ /*
+ * Add divider.
+ */
+ if (mViewAddressBar.getChildCount() > 0) {
+ View divider = inflater.inflate(
+ R.layout.afc_view_locations_divider, null);
+
+ if (lpDivider == null) {
+ lpDivider = new LinearLayout.LayoutParams(dim, dim);
+ lpDivider.gravity = Gravity.CENTER;
+ lpDivider.setMargins(dim, dim, dim, dim);
+ }
+ mViewAddressBar.addView(divider, 0, lpDivider);
+ }
+
+ Uri lastUri = Uri.parse(progress[0].getString(progress[0]
+ .getColumnIndex(BaseFile.COLUMN_URI)));
+
+ TextView btnLoc = (TextView) inflater.inflate(
+ R.layout.afc_button_location, null);
+ String name = BaseFileProviderUtils.getFileName(progress[0]);
+ btnLoc.setText(TextUtils.isEmpty(name) ? getString(R.string.afc_root)
+ : name);
+ btnLoc.setTag(lastUri);
+ btnLoc.setOnClickListener(mBtnLocationOnClickListener);
+ btnLoc.setOnLongClickListener(mBtnLocationOnLongClickListener);
+ mViewAddressBar.addView(btnLoc, 0, lpBtnLoc);
+
+ if (count++ == 0) {
+ Rect r = new Rect();
+ btnLoc.getPaint().getTextBounds(name, 0, name.length(), r);
+ if (r.width() >= getResources().getDimensionPixelSize(
+ R.dimen.afc_button_location_max_width)
+ - btnLoc.getPaddingLeft()
+ - btnLoc.getPaddingRight()) {
+ mTextFullDirName.setText(progress[0]
+ .getString(progress[0]
+ .getColumnIndex(BaseFile.COLUMN_NAME)));
+ mTextFullDirName.setVisibility(View.VISIBLE);
+ } else
+ mTextFullDirName.setVisibility(View.GONE);
+ }// if
+ }// onProgressUpdate()
+
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+
+ /*
+ * Sometimes without delay time, it doesn't work...
+ */
+ mViewLocationsContainer.postDelayed(new Runnable() {
+
+ public void run() {
+ mViewLocationsContainer
+ .fullScroll(HorizontalScrollView.FOCUS_RIGHT);
+ }// run()
+ }, DisplayPrefs.DELAY_TIME_FOR_VERY_SHORT_ANIMATION);
+ }// onPostExecute()
+
+ }.execute();
+ }// buildAddressBar()
+
+ /**
+ * Finishes this activity when save-as.
+ *
+ * @param file
+ * @link Uri.
+ */
+ private void finish(Uri file, boolean fileExists) {
+ ArrayList list = new ArrayList();
+ list.add(file);
+ Intent intent = new Intent();
+ intent.setData(file);
+ intent.putParcelableArrayListExtra(FileChooserActivity.EXTRA_RESULTS,
+ list);
+ intent.putExtra(FileChooserActivity.EXTRA_RESULT_FILE_EXISTS,
+ fileExists);
+ getActivity().setResult(FileChooserActivity.RESULT_OK, intent);
+
+ getActivity().finish();
+ }// finish()
+
+ /**
+ * Finishes this activity.
+ *
+ * @param files
+ * list of {@link Uri}.
+ */
+ private void finish(Uri... files) {
+ List list = new ArrayList();
+ for (Uri uri : files)
+ list.add(uri);
+ finish((ArrayList) list);
+ }// finish()
+
+ /**
+ * Finishes this activity.
+ *
+ * @param files
+ * list of {@link Uri}.
+ */
+ private void finish(ArrayList files) {
+ if (files == null || files.isEmpty()) {
+ getActivity().setResult(Activity.RESULT_CANCELED);
+ getActivity().finish();
+ return;
+ }
+
+ Intent intent = new Intent();
+ if (files.size() == 1)
+ {
+ intent.setData(files.get(0));
+ }
+ intent.putParcelableArrayListExtra(FileChooserActivity.EXTRA_RESULTS,
+ files);
+
+ getActivity().setResult(FileChooserActivity.RESULT_OK, intent);
+
+ if (DisplayPrefs.isRememberLastLocation(getActivity())
+ && getCurrentLocation() != null)
+ DisplayPrefs.setLastLocation(getActivity(), getCurrentLocation()
+ .toString());
+ else
+ DisplayPrefs.setLastLocation(getActivity(), null);
+
+ getActivity().finish();
+ }// finish()
+
+ /*
+ * =========================================================================
+ * BUTTON LISTENERS
+ * =========================================================================
+ */
+
+ private final View.OnClickListener mBtnGoHomeOnClickListener = new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ goHome();
+ }// onClick()
+ };// mBtnGoHomeOnClickListener
+
+
+
+ private final View.OnClickListener mBtnGoBackOnClickListener = new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ /*
+ * If user deleted a dir which was one in history, then maybe there
+ * are duplicates, so we check and remove them here.
+ */
+ Uri currentLoc = getCurrentLocation();
+ Uri preLoc = null;
+
+ while (currentLoc.equals(preLoc = mHistory.prevOf(currentLoc)))
+ mHistory.remove(preLoc);
+
+ if (preLoc != null)
+ goTo(preLoc);
+ else
+ mViewGoBack.setEnabled(false);
+ }
+ };// mBtnGoBackOnClickListener
+
+ private final View.OnClickListener mBtnLocationOnClickListener = new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (v.getTag() instanceof Uri) {
+ goTo((Uri) v.getTag());
+ }
+ }// onClick()
+ };// mBtnLocationOnClickListener
+
+ private final View.OnLongClickListener mBtnLocationOnLongClickListener = new View.OnLongClickListener() {
+
+ @Override
+ public boolean onLongClick(View v) {
+ if (BaseFile.FILTER_FILES_ONLY == mFilterMode || mIsSaveDialog)
+ return false;
+
+ finish((Uri) v.getTag());
+
+ return false;
+ }// onLongClick()
+
+ };// mBtnLocationOnLongClickListener
+
+ private final View.OnClickListener mBtnGoForwardOnClickListener = new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ /*
+ * If user deleted a dir which was one in history, then maybe there
+ * are duplicates, so we check and remove them here.
+ */
+ Uri currentLoc = getCurrentLocation();
+ Uri nextLoc = null;
+
+ while (currentLoc.equals(nextLoc = mHistory.nextOf(currentLoc)))
+ mHistory.remove(nextLoc);
+
+ if (nextLoc != null)
+ goTo(nextLoc);
+ else
+ mViewGoForward.setEnabled(false);
+ }// onClick()
+ };// mBtnGoForwardOnClickListener
+
+
+ private final View.OnClickListener mBtnOk_SaveDialog_OnClickListener = new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Ui.showSoftKeyboard(v, false);
+ checkSaveasFilenameAndFinish();
+ }// onClick()
+ };// mBtnOk_SaveDialog_OnClickListener
+
+ private final View.OnClickListener mBtnOk_OpenDialog_OnClickListener = new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ finish(mFileAdapter.getSelectedItems());
+ }// onClick()
+ };// mBtnOk_OpenDialog_OnClickListener
+
+ /*
+ * FRAGMENT LISTENERS
+ */
+
+
+ /*
+ * LISTVIEW HELPER
+ */
+
+ private final AdapterView.OnItemClickListener mViewFilesOnItemClickListener = new AdapterView.OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position,
+ long id) {
+ Cursor cursor = (Cursor) mFileAdapter.getItem(position);
+
+ if (BaseFileProviderUtils.isDirectory(cursor)) {
+ goTo(BaseFileProviderUtils.getUri(cursor));
+ return;
+ }
+
+ if (mIsSaveDialog) {
+ mTextSaveas.setText(BaseFileProviderUtils.getFileName(cursor));
+ /*
+ * Always set tag after setting text, or tag will be reset to
+ * null.
+ */
+ mTextSaveas.setTag(BaseFileProviderUtils.getUri(cursor));
+ }
+
+ if (mDoubleTapToChooseFiles) {
+ /*
+ * Do nothing.
+ */
+ return;
+ }// double tap to choose files
+ else {
+ if (mIsMultiSelection)
+ return;
+
+ if (mIsSaveDialog)
+ checkSaveasFilenameAndFinish();
+ else
+ finish(BaseFileProviderUtils.getUri(cursor));
+ }// single tap to choose files
+ }// onItemClick()
+ };// mViewFilesOnItemClickListener
+
+ private final AdapterView.OnItemLongClickListener mViewFilesOnItemLongClickListener = new AdapterView.OnItemLongClickListener() {
+
+ @Override
+ public boolean onItemLongClick(AdapterView> parent, View view,
+ int position, long id) {
+ Cursor cursor = (Cursor) mFileAdapter.getItem(position);
+
+ if (mDoubleTapToChooseFiles) {
+ // do nothing
+ }// double tap to choose files
+ else {
+ if (!mIsSaveDialog
+ && !mIsMultiSelection
+ && BaseFileProviderUtils.isDirectory(cursor)
+ && (BaseFile.FILTER_DIRECTORIES_ONLY == mFilterMode || BaseFile.FILTER_FILES_AND_DIRECTORIES == mFilterMode)) {
+ finish(BaseFileProviderUtils.getUri(cursor));
+ }
+ }// single tap to choose files
+
+ /*
+ * Notify that we already handled long click here.
+ */
+ return true;
+ }// onItemLongClick()
+ };// mViewFilesOnItemLongClickListener
+
+
+
+ /**
+ * We use a {@link LoadingDialog} to avoid of
+ * {@code NetworkOnMainThreadException}.
+ */
+ private LoadingDialog mFileSelector;
+
+ /**
+ * Creates new {@link #mFileSelector} to select appropriate file after
+ * loading a folder's content. It's either the parent path of last path, or
+ * the file provided by key {@link FileChooserActivity#EXTRA_SELECT_FILE}.
+ * Note that this also cancels previous selector if there is such one.
+ */
+ private void createFileSelector() {
+ if (mFileSelector != null)
+ mFileSelector.cancel(true);
+
+ mFileSelector = new LoadingDialog(getActivity(),
+ true) {
+
+ @Override
+ protected Integer doInBackground(Void... params) {
+ final Cursor cursor = mFileAdapter.getCursor();
+ if (cursor == null || cursor.isClosed())
+ return -1;
+
+ final Uri selectedFile = (Uri) getArguments().getParcelable(
+ FileChooserActivity.EXTRA_SELECT_FILE);
+ final int colUri = cursor.getColumnIndex(BaseFile.COLUMN_URI);
+ if (selectedFile != null)
+ getArguments()
+ .remove(FileChooserActivity.EXTRA_SELECT_FILE);
+
+ int shouldBeSelectedIdx = -1;
+ final Uri uri = selectedFile != null ? selectedFile
+ : getLastLocation();
+ if (uri == null
+ || !BaseFileProviderUtils
+ .fileExists(getActivity(), uri))
+ return -1;
+
+ final String fileName = BaseFileProviderUtils.getFileName(
+ getActivity(), uri);
+ if (fileName == null)
+ return -1;
+
+ Uri parentUri = BaseFileProviderUtils.getParentFile(
+ getActivity(), uri);
+ if ((uri == getLastLocation()
+ && !getCurrentLocation().equals(getLastLocation()) && BaseFileProviderUtils
+ .isAncestorOf(getActivity(), getCurrentLocation(),
+ uri))
+ || getCurrentLocation().equals(parentUri)) {
+ if (cursor.moveToFirst()) {
+ while (!cursor.isLast()) {
+ if (isCancelled())
+ return -1;
+
+ Uri subUri = Uri.parse(cursor.getString(colUri));
+ if (uri == getLastLocation()) {
+ if (cursor.getInt(cursor
+ .getColumnIndex(BaseFile.COLUMN_TYPE)) == BaseFile.FILE_TYPE_DIRECTORY) {
+ if (subUri.equals(uri)
+ || BaseFileProviderUtils
+ .isAncestorOf(
+ getActivity(),
+ subUri, uri)) {
+ shouldBeSelectedIdx = Math.max(0,
+ cursor.getPosition() - 2);
+ break;
+ }
+ }
+ } else {
+ if (uri.equals(subUri)) {
+ shouldBeSelectedIdx = Math.max(0,
+ cursor.getPosition() - 2);
+ break;
+ }
+ }
+
+ cursor.moveToNext();
+ }// while
+ }// if
+ }// if
+
+ return shouldBeSelectedIdx;
+ }// doInBackground()
+
+ @Override
+ protected void onPostExecute(final Integer result) {
+ super.onPostExecute(result);
+
+ if (isCancelled() || mFileAdapter.isEmpty())
+ return;
+
+ /*
+ * Use a Runnable to make sure this works. Because if the list
+ * view is handling data, this might not work.
+ *
+ * Also sometimes it doesn't work without a delay.
+ */
+ mViewFiles.postDelayed(new Runnable() {
+
+ @Override
+ public void run() {
+ if (result >= 0 && result < mFileAdapter.getCount())
+ mViewFiles.setSelection(result);
+ else if (!mFileAdapter.isEmpty())
+ mViewFiles.setSelection(0);
+ }// run()
+ }, DisplayPrefs.DELAY_TIME_FOR_VERY_SHORT_ANIMATION);
+ }// onPostExecute()
+
+ };
+
+ mFileSelector.execute();
+ }// createFileSelector()
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/prefs/DisplayPrefs.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/prefs/DisplayPrefs.java
new file mode 100644
index 00000000..72d0ec98
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/prefs/DisplayPrefs.java
@@ -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
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/prefs/Prefs.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/prefs/Prefs.java
new file mode 100644
index 00000000..983edd9d
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/prefs/Prefs.java
@@ -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()
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/BaseColumns.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/BaseColumns.java
new file mode 100644
index 00000000..b7e3bc79
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/BaseColumns.java
@@ -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.
+ *
+ * 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.
+ *
+ * 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";
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/BaseFileProviderUtils.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/BaseFileProviderUtils.java
new file mode 100644
index 00000000..d06988fe
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/BaseFileProviderUtils.java
@@ -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.
+ *
+ * Note for developers: If you provide your own provider, use
+ * {@link #registerProviderInfo(String, String)} to register it..
+ */
+ private static final Map MAP_PROVIDER_INFO = new HashMap();
+
+ 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 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.
+ *
+ * Note: 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.
+ *
+ * The column orders are:
+ *
+ *
+ *
{@link BaseFile#_ID}
+ *
{@link BaseFile#COLUMN_URI}
+ *
{@link BaseFile#COLUMN_REAL_URI}
+ *
{@link BaseFile#COLUMN_NAME}
+ *
{@link BaseFile#COLUMN_CAN_READ}
+ *
{@link BaseFile#COLUMN_CAN_WRITE}
+ *
{@link BaseFile#COLUMN_SIZE}
+ *
{@link BaseFile#COLUMN_TYPE}
+ *
{@link BaseFile#COLUMN_MODIFICATION_TIME}
+ *
{@link BaseFile#COLUMN_ICON_ID}
+ *
+ */
+ 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()
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/DbUtils.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/DbUtils.java
new file mode 100644
index 00000000..640be242
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/DbUtils.java
@@ -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()
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/ProviderUtils.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/ProviderUtils.java
new file mode 100644
index 00000000..944be962
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/ProviderUtils.java
@@ -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()
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/basefile/BaseFileContract.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/basefile/BaseFileContract.java
new file mode 100644
index 00000000..705b3993
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/basefile/BaseFileContract.java
@@ -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
+ */
+
+ /**
+ * This is internal field.
+ *
+ * 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";
+ /**
+ * This is internal field.
+ *
+ * The path to a single file. This can be a file or a directory.
+ */
+ public static final String PATH_FILE = "file";
+ /**
+ * This is internal field.
+ *
+ * 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 a closed cursor if the
+ * given source file is a directory and it is ancestor of the target
+ * file.
+ *
+ * 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}.
+ *
+ * For example, with local file, this query returns {@code true}:
+ *
+ * {@code content://local-file-authority/api/is_ancestor_of?source="/mnt/sdcard"&target="/mnt/sdcard/Android/data/cache"}
+ *
+ * Note that no matter how many levels between the ancestor and the
+ * descendant are, it is still the ancestor. This is not
+ * 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.
+ *
+ * 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 Android handles ContentProvider startup
+ * and shutdown automatically. 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.
+ *
+ * Type: URI
+ */
+ public static final String PARAM_SOURCE = "source";
+
+ /**
+ * Use this parameter to provide the target file ID.
+ *
+ * Type: URI
+ */
+ public static final String PARAM_TARGET = "target";
+
+ /**
+ * Use this parameter to provide the name of new file/ directory you
+ * want to create.
+ *
+ * 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.
+ *
+ * Default: {@code 0} with all methods.
+ *
+ * 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}.
+ *
+ * Default:
+ *
+ *
+ *
{@code "true"} with {@code delete()}.
+ *
+ *
+ * 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}.
+ *
+ * Default: {@code "false"} with {@code query()}.
+ *
+ * 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}.
+ *
+ * Default: {@link #FILTER_FILES_AND_DIRECTORIES} with {@code query()}.
+ *
+ * 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}.
+ *
+ * Default: {@link #SORT_BY_NAME} with {@code query()}.
+ *
+ * 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}.
+ *
+ * Default: {@code "true"} with {@code query()}.
+ *
+ * Type: {@code Boolean}
+ */
+ public static final String PARAM_SORT_ASCENDING = "sort_ascending";
+
+ /**
+ * Use this parameter to limit results.
+ *
+ * Default: {@code 1000} with {@code query()}.
+ *
+ * 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}.
+ *
+ * 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.
+ *
+ * This parameter can be use together with {@link #PARAM_APPEND_PATH},
+ * the priority is lesser than that parameter.
+ *
+ *
+ *
+ * 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)}.
+ *
+ * This parameter can be use together with {@link #PARAM_APPEND_NAME},
+ * the priority is higher than that parameter.
+ *
+ *
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * Type: {@code String} - can be {@code "true"} or {@code "1"} for
+ * {@code true}, {@code "false"} or {@code "0"} for {@code false}.
+ *
+ * Scope:
+ * {@link ContentResolver#query(Uri, String[], String, String[], String)}
+ * and related.
+ *
+ * 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,
+ * not 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.
+ *
+ * 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.
+ *
+ * Type: {@code String}
+ */
+ public static final String COLUMN_REAL_URI = "real_uri";
+
+ /**
+ * The name of this file.
+ *
+ * Type: {@code String}
+ */
+ public static final String COLUMN_NAME = "name";
+
+ /**
+ * Size of this file.
+ *
+ * Type: {@code Long}
+ */
+ public static final String COLUMN_SIZE = "size";
+
+ /**
+ * Holds the readable attribute of this file, {@code 0 == false} and
+ * {@code 1 == true}.
+ *
+ * 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}.
+ *
+ * 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}.
+ *
+ * Type: {@code Integer}
+ */
+ public static final String COLUMN_TYPE = "type";
+
+ /**
+ * The resource ID of the file icon.
+ *
+ * Type: {@code Integer}
+ */
+ public static final String COLUMN_ICON_ID = "icon_id";
+
+ /**
+ * The name of this provider.
+ *
+ * Type: {@code String}
+ */
+ public static final String COLUMN_PROVIDER_NAME = "provider_name";
+
+ /**
+ * The ID of this provider.
+ *
+ * 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.
+ *
+ * Type: {@code Integer}
+ */
+ public static final String COLUMN_PROVIDER_ICON_ATTR = "provider_icon_attr";
+ }// BaseFile
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/basefile/BaseFileProvider.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/basefile/BaseFileProvider.java
new file mode 100644
index 00000000..389aa755
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/basefile/BaseFileProvider.java
@@ -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()
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/history/HistoryContract.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/history/HistoryContract.java
new file mode 100644
index 00000000..36552f7f
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/history/HistoryContract.java
@@ -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.
+ *
+ * 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}.
+ *
+ * Type: {@code Integer}
+ */
+ public static final String COLUMN_FILE_TYPE = "file_type";
+
+ /**
+ * Column name for the URI of history.
+ *
+ * Type: {@code URI}
+ */
+ public static final String COLUMN_URI = "uri";
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/history/HistoryHelper.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/history/HistoryHelper.java
new file mode 100644
index 00000000..53a369c0
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/history/HistoryHelper.java
@@ -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()
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/history/HistoryProvider.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/history/HistoryProvider.java
new file mode 100644
index 00000000..0475b412
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/history/HistoryProvider.java
@@ -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 MAP_COLUMNS = new HashMap();
+
+ 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 = " 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()
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/history/HistoryProviderUtils.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/history/HistoryProviderUtils.java
new file mode 100644
index 00000000..9cdbf0d1
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/history/HistoryProviderUtils.java
@@ -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()
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/localfile/FileObserverEx.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/localfile/FileObserverEx.java
new file mode 100644
index 00000000..33c84323
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/localfile/FileObserverEx.java
@@ -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()
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/localfile/HandlerThreadCompat_v5.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/localfile/HandlerThreadCompat_v5.java
new file mode 100644
index 00000000..41a9e874
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/localfile/HandlerThreadCompat_v5.java
@@ -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()
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/localfile/LocalFileContract.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/localfile/LocalFileContract.java
new file mode 100644
index 00000000..4c5f5e33
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/localfile/LocalFileContract.java
@@ -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";
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/localfile/LocalFileProvider.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/localfile/LocalFileProvider.java
new file mode 100644
index 00000000..f084ba1a
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/providers/localfile/LocalFileProvider.java
@@ -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 files = new ArrayList();
+ 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 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 files,
+ final boolean ascending, final int sortBy) {
+ try {
+ Collections.sort(files, new Comparator() {
+
+ @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
+ * non-null but empty 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()
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/ui/widget/AfcSearchView.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/ui/widget/AfcSearchView.java
new file mode 100644
index 00000000..133e5e2e
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/ui/widget/AfcSearchView.java
@@ -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.
+ *
+ * Note: This method is called before setting the new search
+ * query to last search query (which can be obtained with
+ * {@link AfcSearchView#getSearchText()}).
+ *
+ *
+ * @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 expanded 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
+
+}
diff --git a/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/utils/Converter.java b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/utils/Converter.java
new file mode 100644
index 00000000..42e5a41f
--- /dev/null
+++ b/src/java/android-filechooser-AS/app/src/main/java/group/pals/android/lib/ui/filechooser/utils/Converter.java
@@ -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.:
+ *
+ *