Browse Source

Merge branch 'develop' into imageGrid

David A. Velasco 10 năm trước cách đây
mục cha
commit
4af15f7b4e
72 tập tin đã thay đổi với 3150 bổ sung967 xóa
  1. 4 0
      .gitmodules
  2. 6 0
      res/values-ca/strings.xml
  3. 1 0
      res/values-cs-rCZ/strings.xml
  4. 1 0
      res/values-da/strings.xml
  5. 1 0
      res/values-de-rDE/strings.xml
  6. 1 0
      res/values-de/strings.xml
  7. 1 0
      res/values-el/strings.xml
  8. 1 0
      res/values-en-rGB/strings.xml
  9. 3 2
      res/values-es/strings.xml
  10. 4 0
      res/values-eu/strings.xml
  11. 3 0
      res/values-fi-rFI/strings.xml
  12. 5 4
      res/values-fr/strings.xml
  13. 1 0
      res/values-gl/strings.xml
  14. 19 0
      res/values-hr/strings.xml
  15. 1 0
      res/values-it/strings.xml
  16. 2 1
      res/values-ja-rJP/strings.xml
  17. 14 0
      res/values-km/strings.xml
  18. 1 0
      res/values-ko/strings.xml
  19. 1 0
      res/values-nl/strings.xml
  20. 1 0
      res/values-pt-rBR/strings.xml
  21. 15 14
      res/values-ru/strings.xml
  22. 7 0
      res/values-sk-rSK/strings.xml
  23. 1 0
      res/values-tr/strings.xml
  24. 1 0
      res/values-zh-rCN/strings.xml
  25. 1 0
      res/values/strings.xml
  26. 10 12
      res/xml/preferences.xml
  27. 4 4
      src/com/owncloud/android/authentication/AuthenticatorActivity.java
  28. 59 8
      src/com/owncloud/android/datamodel/FileDataStorageManager.java
  29. 19 12
      src/com/owncloud/android/datamodel/OCFile.java
  30. 2 1
      src/com/owncloud/android/db/ProviderMeta.java
  31. 10 6
      src/com/owncloud/android/files/FileMenuFilter.java
  32. 43 24
      src/com/owncloud/android/files/FileOperationsHelper.java
  33. 181 127
      src/com/owncloud/android/files/services/FileDownloader.java
  34. 5 5
      src/com/owncloud/android/files/services/FileUploader.java
  35. 225 0
      src/com/owncloud/android/files/services/IndexedForest.java
  36. 610 0
      src/com/owncloud/android/operations/RefreshFolderOperation.java
  37. 3 4
      src/com/owncloud/android/operations/RenameFileOperation.java
  38. 74 14
      src/com/owncloud/android/operations/SynchronizeFileOperation.java
  39. 263 341
      src/com/owncloud/android/operations/SynchronizeFolderOperation.java
  40. 23 2
      src/com/owncloud/android/providers/FileContentProvider.java
  41. 341 240
      src/com/owncloud/android/services/OperationsService.java
  42. 213 0
      src/com/owncloud/android/services/SyncFolderHandler.java
  43. 2 2
      src/com/owncloud/android/syncadapter/FileSyncAdapter.java
  44. 9 6
      src/com/owncloud/android/ui/activity/ComponentsGetter.java
  45. 13 2
      src/com/owncloud/android/ui/activity/FileActivity.java
  46. 36 13
      src/com/owncloud/android/ui/activity/FileDisplayActivity.java
  47. 6 6
      src/com/owncloud/android/ui/activity/FolderPickerActivity.java
  48. 55 1
      src/com/owncloud/android/ui/activity/Preferences.java
  49. 11 106
      src/com/owncloud/android/ui/adapter/FileListListAdapter.java
  50. 5 1
      src/com/owncloud/android/ui/fragment/FileDetailFragment.java
  51. 1 1
      src/com/owncloud/android/ui/fragment/OCFileListFragment.java
  52. 4 3
      src/com/owncloud/android/ui/preview/FileDownloadFragment.java
  53. 1 1
      src/com/owncloud/android/ui/preview/PreviewImageActivity.java
  54. 16 0
      src/com/owncloud/android/utils/ErrorMessageAdapter.java
  55. 20 4
      src/com/owncloud/android/utils/FileStorageUtils.java
  56. 173 0
      user_manual/Makefile
  57. 115 0
      user_manual/android_app.rst
  58. 293 0
      user_manual/conf.py
  59. BIN
      user_manual/images/android-downloads.png
  60. BIN
      user_manual/images/android-file-list.png
  61. BIN
      user_manual/images/android-file-options.png
  62. BIN
      user_manual/images/android-file.png
  63. BIN
      user_manual/images/android-files-page.png
  64. BIN
      user_manual/images/android-first-screen.jpg
  65. BIN
      user_manual/images/android-help.png
  66. BIN
      user_manual/images/android-new-account.png
  67. BIN
      user_manual/images/android-settings.png
  68. BIN
      user_manual/images/android-ssl-cert.png
  69. BIN
      user_manual/images/android-upload.png
  70. 9 0
      user_manual/index.rst
  71. 199 0
      user_manual/make.bat
  72. 1 0
      user_manual/ocdoc

+ 4 - 0
.gitmodules

@@ -3,3 +3,7 @@
 	path = owncloud-android-library
 	url = git://github.com/owncloud/android-library.git
 	branch = develop
+[submodule "ocdoc"]
+	path = user_manual/ocdoc
+	url = https://github.com/owncloud/documentation
+	branch = master

+ 6 - 0
res/values-ca/strings.xml

@@ -11,6 +11,12 @@
   <string name="actionbar_settings">Configuració</string>
   <string name="actionbar_see_details">Detalls</string>
   <string name="actionbar_send_file">Envia</string>
+  <string name="actionbar_sort">Ordena</string>
+  <string name="actionbar_sort_title">Ordena per</string>
+  <string-array name="actionbar_sortby">
+    <item>A-Z</item>
+    <item>Més nou - Més antic</item>
+  </string-array>
   <!--TODO re-enable when server-side folder size calculation is available   
     	<item>Biggest - Smallest</item>-->
   <string name="prefs_category_general">General</string>

+ 1 - 0
res/values-cs-rCZ/strings.xml

@@ -297,6 +297,7 @@ správce systému.</string>
   <string name="prefs_category_instant_uploading">Okamžitá odesílání</string>
   <string name="prefs_category_security">Zabezpečení</string>
   <string name="prefs_instant_video_upload_path_title">Cesta pro nahrávání videí</string>
+  <string name="download_folder_failed_content">Stažení adresáře %1$s nemohlo být dokončeno</string>
   <string name="shared_subject_header">sdílené</string>
   <string name="with_you_subject_header">s vámi</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 1 - 0
res/values-da/strings.xml

@@ -296,6 +296,7 @@
   <string name="prefs_category_instant_uploading">Øjeblikkelige uploads</string>
   <string name="prefs_category_security">Sikkerhed</string>
   <string name="prefs_instant_video_upload_path_title">Sti til videoupload</string>
+  <string name="download_folder_failed_content">Download af %1$s mappe kunne ikke fuldføres</string>
   <string name="shared_subject_header">delt</string>
   <string name="with_you_subject_header">med dig</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 1 - 0
res/values-de-rDE/strings.xml

@@ -298,6 +298,7 @@
   <string name="prefs_category_instant_uploading">Sofortiges Hochladen</string>
   <string name="prefs_category_security">Sicherheit</string>
   <string name="prefs_instant_video_upload_path_title">Verzeichnis zum Hochladen der Videos</string>
+  <string name="download_folder_failed_content">Herunterladen des %1$s - Ordners konnte nicht abgeschlossen werden</string>
   <string name="shared_subject_header">geteilt</string>
   <string name="with_you_subject_header">Mit Ihnen</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 1 - 0
res/values-de/strings.xml

@@ -298,6 +298,7 @@
   <string name="prefs_category_instant_uploading">Sofortiges Hochladen</string>
   <string name="prefs_category_security">Sicherheit</string>
   <string name="prefs_instant_video_upload_path_title">Verzeichnis zum Hochladen der Videos</string>
+  <string name="download_folder_failed_content">Herunterladen des %1$s - Ordners konnte nicht abgeschlossen werden</string>
   <string name="shared_subject_header">geteilt</string>
   <string name="with_you_subject_header">Mit Dir</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 1 - 0
res/values-el/strings.xml

@@ -298,6 +298,7 @@
   <string name="prefs_category_instant_uploading">Στιγμιαίες Μεταφορτώσεις</string>
   <string name="prefs_category_security">Ασφάλεια</string>
   <string name="prefs_instant_video_upload_path_title">Διαδρομή Μεταφόρτωσης Βίντεο</string>
+  <string name="download_folder_failed_content">Η λήψη του φακέλου %1$s δεν ολοκληρώθηκε με επιτυχία.</string>
   <string name="shared_subject_header">μοιρασμένο </string>
   <string name="with_you_subject_header">με εσένα</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 1 - 0
res/values-en-rGB/strings.xml

@@ -298,6 +298,7 @@
   <string name="prefs_category_instant_uploading">Instant Uploads</string>
   <string name="prefs_category_security">Security</string>
   <string name="prefs_instant_video_upload_path_title">Upload Video Path</string>
+  <string name="download_folder_failed_content">Download of %1$s folder could not be completed</string>
   <string name="shared_subject_header">shared</string>
   <string name="with_you_subject_header">with you</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 3 - 2
res/values-es/strings.xml

@@ -46,7 +46,7 @@
   <string name="auth_host_url">Dirección del servidor https://…</string>
   <string name="auth_username">Nombre de usuario</string>
   <string name="auth_password">Contraseña</string>
-  <string name="auth_register">¿Nuevo para %1$s?</string>
+  <string name="auth_register">¿Nuevo en %1$s?</string>
   <string name="sync_string_files">Archivos</string>
   <string name="setup_btn_connect">Conectar</string>
   <string name="uploader_btn_upload_text">Subir</string>
@@ -297,7 +297,8 @@
   <string name="forbidden_permissions_move">para mover este archivo</string>
   <string name="prefs_category_instant_uploading">Subidas instantáneas</string>
   <string name="prefs_category_security">Seguridad</string>
-  <string name="prefs_instant_video_upload_path_title">Ruta de vídeo de subida</string>
+  <string name="prefs_instant_video_upload_path_title">Guardar videos subidos en la carpeta:</string>
+  <string name="download_folder_failed_content">Descarga de la carpeta %1$s no ha podido ser completada</string>
   <string name="shared_subject_header">compartido</string>
   <string name="with_you_subject_header">contigo</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 4 - 0
res/values-eu/strings.xml

@@ -271,6 +271,8 @@ Mesedez, baimendu berriz</string>
   <string name="downloader_download_file_not_found">Fitxategia jadanik ez dago eskuragarri zerbitzarian</string>
   <string name="prefs_category_accounts">Kontuak</string>
   <string name="prefs_add_account">Gehitu kontua</string>
+  <string name="log_send_mail_subject">%1$s Android aplikazioaren egunerokoak</string>
+  <string name="log_progress_dialog_text">Datuak kargatzen...</string>
   <string name="saml_authentication_required_text">Autentikazioa beharrezkoa</string>
   <string name="saml_authentication_wrong_pass">Pasahitz okerra</string>
   <string name="actionbar_move">Mugitu</string>
@@ -278,4 +280,6 @@ Mesedez, baimendu berriz</string>
   <string name="prefs_category_instant_uploading">Berehalako Igoerak</string>
   <string name="prefs_category_security">Segurtasuna</string>
   <string name="shared_subject_header">konpartitua</string>
+  <string name="with_you_subject_header">zurekin</string>
+  <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>
 </resources>

+ 3 - 0
res/values-fi-rFI/strings.xml

@@ -131,12 +131,15 @@
   <string name="pincode_wrong">Väärä sovelluksen PIN</string>
   <string name="pincode_removed">Sovelluksen PIN poistettu</string>
   <string name="pincode_stored">Sovelluksen PIN-koodi tallennettu</string>
+  <string name="media_notif_ticker">%1$s-musiikkisoitin</string>
   <string name="media_state_playing">%1$s (toistetaan)</string>
   <string name="media_state_loading">%1$s (ladataan)</string>
   <string name="media_err_nothing_to_play">Mediatiedostoa ei löytynyt</string>
   <string name="media_err_no_account">Tiliä ei määritetty</string>
   <string name="media_err_not_in_owncloud">Tiedosto ei ole kelvollisella tilillä</string>
+  <string name="media_err_unsupported">Mediakoodekki ei ole tuettu</string>
   <string name="media_err_io">Mediatiedoston luku ei onnistunut</string>
+  <string name="media_err_malformed">Mediatiedostoa ei ole koodattu kelvollisesti</string>
   <string name="media_err_timeout">Aikakatkaisu toistoa yrittäessä</string>
   <string name="media_err_invalid_progressive_playback">Mediatiedostoa ei voi suoratoistaa</string>
   <string name="media_err_security_ex">Turvallisuusvirhe yrittäessä toistaa kohdetta %1$s</string>

+ 5 - 4
res/values-fr/strings.xml

@@ -112,10 +112,10 @@ Téléchargez-le ici : %2$s</string>
   <string name="common_choose_account">Choisissez un compte</string>
   <string name="sync_fail_ticker">La synchronisation a échoué</string>
   <string name="sync_fail_ticker_unauthorized">Échec de la synchronisation, vous devez vous reconnecter à nouveau</string>
-  <string name="sync_fail_content">La synchronisation de %1$s ne peut pas être complétée</string>
-  <string name="sync_fail_content_unauthorized">Mot de passe invalide pour %1$s</string>
+  <string name="sync_fail_content">La synchronisation de %1$s n\'a pu être terminée</string>
+  <string name="sync_fail_content_unauthorized">Mot de passe non valide pour %1$s</string>
   <string name="sync_conflicts_in_favourites_ticker">Des conflits ont été trouvés</string>
-  <string name="sync_conflicts_in_favourites_content">%1$d fichiers à garder synchronisés n\'ont put être synchronisé</string>
+  <string name="sync_conflicts_in_favourites_content">%1$d fichiers à garder synchronisés n\'ont pu être synchronisés</string>
   <string name="sync_fail_in_favourites_ticker">La synchronisation des fichiers a échoué</string>
   <string name="sync_fail_in_favourites_content">Le contenu de %1$d fichiers n\'a pu être synchronisé (%2$d conflits)</string>
   <string name="sync_foreign_files_forgotten_ticker">Certains fichiers locaux ont été oubliés</string>
@@ -151,7 +151,7 @@ Ci-dessous la liste des fichiers locaux, et les fichiers distants dans %5$s auxq
   <string name="media_err_unsupported">Le codec de ce média n\'est pas pris en charge </string>
   <string name="media_err_io">Le fichier média ne peut pas être lu</string>
   <string name="media_err_malformed">Le fichier média n\'est pas correctement encodé</string>
-  <string name="media_err_timeout">Délai dépassé pour la lecture du morceau.</string>
+  <string name="media_err_timeout">Délai dépassé pour la lecture du morceau</string>
   <string name="media_err_invalid_progressive_playback">Le fichier média ne peut pas être diffusé</string>
   <string name="media_err_unknown">Le fichier média ne peut être joué avec le lecteur standard</string>
   <string name="media_err_security_ex">Erreur de sécurité à la lecture de %1$s</string>
@@ -302,6 +302,7 @@ Ci-dessous la liste des fichiers locaux, et les fichiers distants dans %5$s auxq
   <string name="prefs_category_instant_uploading">Envoi instantané</string>
   <string name="prefs_category_security">Sécurité</string>
   <string name="prefs_instant_video_upload_path_title">Répertoire d\'envoi des vidéos</string>
+  <string name="download_folder_failed_content">Le téléchargement du dossier %1$s n\'a pas pu être achevé complètement</string>
   <string name="shared_subject_header">partagé(e)</string>
   <string name="with_you_subject_header">avec vous</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 1 - 0
res/values-gl/strings.xml

@@ -299,6 +299,7 @@ Descárgueo de aquí: %2$s</string>
   <string name="prefs_category_instant_uploading">Envío instantáneo</string>
   <string name="prefs_category_security">Seguranza</string>
   <string name="prefs_instant_video_upload_path_title">Enviar a ruta do vídeo</string>
+  <string name="download_folder_failed_content">A descarga do cartafol %1$s non se puido completar</string>
   <string name="shared_subject_header">compartido</string>
   <string name="with_you_subject_header">con vostede</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 19 - 0
res/values-hr/strings.xml

@@ -1,16 +1,34 @@
 <?xml version='1.0' encoding='UTF-8'?>
 <resources>
+  <string name="about_android">%1$s Android aplikacija</string>
   <string name="about_version">verzija %1$s</string>
+  <string name="actionbar_sync">Osvježi račun</string>
   <string name="actionbar_upload">Učitaj</string>
+  <string name="actionbar_upload_from_apps">Sadržaj iz drugih aplikacija</string>
   <string name="actionbar_upload_files">Datoteke</string>
+  <string name="actionbar_open_with">Otvori sa</string>
   <string name="actionbar_mkdir">Nova mapa</string>
   <string name="actionbar_settings">Postavke</string>
+  <string name="actionbar_see_details">Detalji</string>
   <string name="actionbar_send_file">Pošaljite</string>
+  <string name="actionbar_sort">Sortiraj</string>
+  <string name="actionbar_sort_title">Sortiraj po</string>
+  <string-array name="actionbar_sortby">
+    <item>A-Z</item>
+    <item>Najnoviji- Stariji</item>
+  </string-array>
   <!--TODO re-enable when server-side folder size calculation is available   
     	<item>Biggest - Smallest</item>-->
   <string name="prefs_category_general">Općenito</string>
   <string name="prefs_category_more">više</string>
   <string name="prefs_accounts">Korisnićki računi</string>
+  <string name="prefs_manage_accounts">Upravljaj računima</string>
+  <string name="prefs_pincode">PIN aplikacije</string>
+  <string name="prefs_pincode_summary">Zaštit svog klijenta</string>
+  <string name="prefs_instant_upload">Trenutni upload slika</string>
+  <string name="prefs_instant_upload_summary">Trenutni upload slika snimljenih kamerom</string>
+  <string name="prefs_instant_video_upload">Trenutni upload videa</string>
+  <string name="prefs_instant_video_upload_summary">Trenutni upload videa snimljen kamerom</string>
   <string name="prefs_help">Pomoć</string>
   <string name="auth_username">Korisničko ime</string>
   <string name="auth_password">Lozinka</string>
@@ -38,6 +56,7 @@
   <string name="auth_trying_to_login">Trying to login…</string>
   <string name="common_rename">Promjeni ime</string>
   <string name="common_remove">Makni</string>
+  <string name="ssl_validator_btn_details_see">Detalji</string>
   <string name="activity_chooser_send_file_title">Pošaljite</string>
   <string name="empty"></string>
   <string name="prefs_category_accounts">Korisnićki računi</string>

+ 1 - 0
res/values-it/strings.xml

@@ -298,6 +298,7 @@
   <string name="prefs_category_instant_uploading">Caricamenti istantanei</string>
   <string name="prefs_category_security">Protezione</string>
   <string name="prefs_instant_video_upload_path_title">Percorso di caricamento video</string>
+  <string name="download_folder_failed_content">Lo scaricamento della cartella %1$s non può essere completato</string>
   <string name="shared_subject_header">condiviso</string>
   <string name="with_you_subject_header">con te</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 2 - 1
res/values-ja-rJP/strings.xml

@@ -283,7 +283,7 @@
   <string name="auth_redirect_non_secure_connection_title">暗号化接続は非暗号化接続にリダイレクトされました。</string>
   <string name="actionbar_logger">ログ</string>
   <string name="log_send_history_button">ログを送信</string>
-  <string name="log_send_no_mail_app">ログを送るアプリが見つかりませんでした。メールアプリをインストールしてさい。</string>
+  <string name="log_send_no_mail_app">ログを送信するアプリが見つかりませんでした。メールアプリをインストールしてください。</string>
   <string name="log_send_mail_subject">%1$s アンドロイドアプリログ</string>
   <string name="log_progress_dialog_text">読込中 ...</string>
   <string name="saml_authentication_required_text">認証を必要とする</string>
@@ -299,6 +299,7 @@
   <string name="prefs_category_instant_uploading">自動アップロード</string>
   <string name="prefs_category_security">セキュリティ</string>
   <string name="prefs_instant_video_upload_path_title">動画のアップロードパス</string>
+  <string name="download_folder_failed_content">%1$s のフォルダのダウンロードが完了しませんでした。</string>
   <string name="shared_subject_header">共有中</string>
   <string name="with_you_subject_header">あなたと</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 14 - 0
res/values-km/strings.xml

@@ -1,17 +1,27 @@
 <?xml version='1.0' encoding='UTF-8'?>
 <resources>
+  <string name="about_android">%1$s កម្មវិធីអានដ្រយ</string>
+  <string name="about_version">ជំនាន់ %1$s</string>
   <string name="actionbar_upload">ផ្ទុក​ឡើង</string>
   <string name="actionbar_upload_files">ឯកសារ</string>
+  <string name="actionbar_open_with">បើកជាមួយ</string>
   <string name="actionbar_mkdir">ថត​ថ្មី</string>
   <string name="actionbar_settings">ការកំណត់</string>
   <string name="actionbar_see_details">ព័ត៌មាន​លម្អិត</string>
   <string name="actionbar_send_file">ផ្ញើ</string>
+  <string name="actionbar_sort">តម្រៀប</string>
+  <string name="actionbar_sort_title">តម្រៀបដោយ</string>
+  <string-array name="actionbar_sortby">
+    <item>A-Z</item>
+    <item>ថ្មីបំផុត-ចាស់បំផុត</item>
+  </string-array>
   <!--TODO re-enable when server-side folder size calculation is available   
     	<item>Biggest - Smallest</item>-->
   <string name="prefs_category_general">ទូទៅ</string>
   <string name="prefs_category_more">ច្រើន​ទៀត</string>
   <string name="prefs_accounts">គណនី</string>
   <string name="prefs_manage_accounts">គ្រប់គ្រង​គណនី</string>
+  <string name="prefs_pincode">ភីន​កូដ កម្មវិធី</string>
   <string name="prefs_help">ជំនួយ</string>
   <string name="auth_username">ឈ្មោះ​អ្នកប្រើ</string>
   <string name="auth_password">ពាក្យសម្ងាត់</string>
@@ -41,6 +51,7 @@
   <string name="common_error">កំហុស</string>
   <string name="common_loading">កំពុងដំណើរការ</string>
   <string name="common_error_unknown">មិន​ស្គាល់​កំហុស</string>
+  <string name="about_title">អំពី</string>
   <string name="change_password">ប្តូរ​ពាក្យសម្ងាត់</string>
   <string name="delete_account">លប់គណនី</string>
   <string name="create_account">បង្កើតគណនី</string>
@@ -69,6 +80,9 @@
   <string name="fd_keep_in_sync">រក្សាឯកសាររហូតដល់កាលបរិច្ឆេទ</string>
   <string name="common_rename">ប្ដូរ​ឈ្មោះ</string>
   <string name="common_remove">ដកចេញ</string>
+  <string name="confirmation_remove_local">ទីកន្លែងតែមួយ</string>
+  <string name="confirmation_remove_remote">ដកចេញពី​សឺវឺ</string>
+  <string name="confirmation_remove_remote_and_local">បញ្ជារ និងទីតាំង</string>
   <string name="remove_success_msg">ការដកយកចេញបានជោគជ័យ</string>
   <string name="remove_fail_msg">ការដកយកចេញបានបរាជ័យ</string>
   <string name="rename_dialog_title">បញ្ចូលឈ្មោះថ្មី</string>

+ 1 - 0
res/values-ko/strings.xml

@@ -297,6 +297,7 @@
   <string name="prefs_category_instant_uploading">즉시 업로드</string>
   <string name="prefs_category_security">보안</string>
   <string name="prefs_instant_video_upload_path_title">동영상 업로드 경로</string>
+  <string name="download_folder_failed_content">%1$s 폴더를 다운로드할 수 없습니다</string>
   <string name="shared_subject_header">공유됨</string>
   <string name="with_you_subject_header">나와</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 1 - 0
res/values-nl/strings.xml

@@ -301,6 +301,7 @@ Hieronder staan de lokale bestanden en de externe bestanden in %5$s waar ze naar
   <string name="prefs_category_instant_uploading">Directe uploads</string>
   <string name="prefs_category_security">Beveiliging</string>
   <string name="prefs_instant_video_upload_path_title">Upload Video Pad</string>
+  <string name="download_folder_failed_content">Download van %1$s map kon niet worden voltooid</string>
   <string name="shared_subject_header">gedeeld</string>
   <string name="with_you_subject_header">met u</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 1 - 0
res/values-pt-rBR/strings.xml

@@ -298,6 +298,7 @@
   <string name="prefs_category_instant_uploading">Envios Instantâneos</string>
   <string name="prefs_category_security">Segurança</string>
   <string name="prefs_instant_video_upload_path_title">Enviar o Caminho do Vídeo</string>
+  <string name="download_folder_failed_content">Baixar %1$s da pasta não pode ser completado</string>
   <string name="shared_subject_header">compartilhado</string>
   <string name="with_you_subject_header">com você</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 15 - 14
res/values-ru/strings.xml

@@ -77,7 +77,7 @@
   <string name="filedetails_sync_file">Обновить файл</string>
   <string name="filedetails_renamed_in_upload_msg">Файл был переименован в %1$s во время загрузки</string>
   <string name="action_share_file">Поделиться ссылкой</string>
-  <string name="action_unshare_file">Удалить ссылку</string>
+  <string name="action_unshare_file">Убрать ссылку</string>
   <string name="common_yes">Да</string>
   <string name="common_no">Нет</string>
   <string name="common_ok">ОК</string>
@@ -115,11 +115,11 @@
   <string name="sync_fail_content">Синхронизация %1$s не может быть завершена</string>
   <string name="sync_fail_content_unauthorized">Неверный пароль для %1$s</string>
   <string name="sync_conflicts_in_favourites_ticker">Обнаружены конфликты</string>
-  <string name="sync_conflicts_in_favourites_content">%1$d файлы не могут быть синхронизированы</string>
+  <string name="sync_conflicts_in_favourites_content">%1$d файлов не может быть синхронизировано</string>
   <string name="sync_fail_in_favourites_ticker">Не удалось синхронизировать файлы</string>
   <string name="sync_fail_in_favourites_content">Содержимое %1$d файлов не может быть синхронизировано (конфликтов: %2$d)</string>
-  <string name="sync_foreign_files_forgotten_ticker">Некоторые локальные файлы были проигнорированы</string>
-  <string name="sync_foreign_files_forgotten_content"> Не возможно скопировать %1$d файлы из %2$s папки</string>
+  <string name="sync_foreign_files_forgotten_ticker">Некоторые загруженные файлы не были перенесены в локальную папку </string>
+  <string name="sync_foreign_files_forgotten_content"> Невозможно скопировать %1$d файлов из папки %2$s</string>
   <string name="sync_foreign_files_forgotten_explanation">Начиная с версии 1.3.16, файлы, загружаемые с этого устройства, копируются в локальный каталог %1$s, чтобы предотвратить потерю данных при синхронизации файла с несколькими учётными записями.\n\nПоэтому все файлы, загруженные предыдущими версиями данного приложения, были скопированы в каталог %2$s. Однако, во время синхронизации что-то помешало завершить эту операцию. Можете оставить файлы как есть и удалить ссылку на %3$s, либо переместить их в %1$s и сохранить ссылку на %4$s.\n\nНиже перечислены локальные файлы, и соответствующие им удалённые файлы в %5$s, к которым они привязаны.</string>
   <string name="sync_current_folder_was_removed">Каталог %1$s больше не существует</string>
   <string name="foreign_files_move">Переместить всё</string>
@@ -141,8 +141,8 @@
   <string name="media_state_playing">%1$s (проигрывается)</string>
   <string name="media_state_loading">%1$s (загружается)</string>
   <string name="media_event_done">%1$s воспроизведение завершено</string>
-  <string name="media_err_nothing_to_play">Медиафайлов не найдено</string>
-  <string name="media_err_no_account">Учётная запись не настроена</string>
+  <string name="media_err_nothing_to_play">Медиафайлы не найдены</string>
+  <string name="media_err_no_account">Учётная запись не указана</string>
   <string name="media_err_not_in_owncloud">Файл в неверной учётной записи</string>
   <string name="media_err_unsupported">Неподдерживаемый кодек</string>
   <string name="media_err_io">Медиафайл не может быть прочитан</string>
@@ -171,7 +171,7 @@
   <string name="auth_timeout_title">Сервер слишком долго не отвечает</string>
   <string name="auth_incorrect_address_title">Неверный URL</string>
   <string name="auth_ssl_general_error_title">Ошибка инициализации SSL</string>
-  <string name="auth_ssl_unverified_server_title">Невозможно проверить SSL-сертификат сервера</string>
+  <string name="auth_ssl_unverified_server_title">Невозможно проверить SSL подлинность сервера</string>
   <string name="auth_bad_oc_version_title">Неизвестная версия сервера</string>
   <string name="auth_wrong_connection_title">Не удается установить соединение</string>
   <string name="auth_secure_connection">Защищённое соединение установлено</string>
@@ -180,12 +180,12 @@
   <string name="auth_oauth_error_access_denied">Сервер авторизации отказал в доступе</string>
   <string name="auth_wtf_reenter_URL">Неожиданный ответ; введите адрес сервера ещё раз</string>
   <string name="auth_expired_oauth_token_toast">Время авторизации истекло. Пожалуйста, авторизуйтесь снова</string>
-  <string name="auth_expired_basic_auth_toast">Пожалуйста, введите пароль</string>
+  <string name="auth_expired_basic_auth_toast">Пожалуйста, укажите текущий пароль</string>
   <string name="auth_expired_saml_sso_token_toast">Время сессии истекло. Пожалуйста, подключитесь снова</string>
   <string name="auth_connecting_auth_server">Подключение к серверу аутентификации...</string>
   <string name="auth_unsupported_auth_method">Сервер не поддерживает выбранный метод аутентификации</string>
-  <string name="auth_unsupported_multiaccount">%1$s не поддерживает сразу несколько учётных записей</string>
-  <string name="auth_fail_get_user_name">Cервер не возвращает корректный пользовательский идентификатор. Пожалуйста, свяжитесь с вашим администратором
+  <string name="auth_unsupported_multiaccount">%1$s не поддерживает несколько учётных записей</string>
+  <string name="auth_fail_get_user_name">Сервер вернул некорректный пользовательский идентификатор. Пожалуйста, свяжитесь с вашим администратором
 ⇥</string>
   <string name="auth_can_not_auth_against_server">Невозможно авторизоваться на этом сервере</string>
   <string name="fd_keep_in_sync">Обновлять файл</string>
@@ -204,7 +204,7 @@
   <string name="rename_server_fail_msg">Переименование не может быть завершено</string>
   <string name="sync_file_fail_msg">Удаленный файл не может быть проверен</string>
   <string name="sync_file_nothing_to_do_msg">Содержимое файла уже синхронизировано</string>
-  <string name="create_dir_fail_msg">Не возможно создать каталог</string>
+  <string name="create_dir_fail_msg">Невозможно создать каталог</string>
   <string name="filename_forbidden_characters">Недопустимые символы: / \\ &lt; &gt; : \" | ? *</string>
   <string name="filename_empty">Имя файла не может быть пустым</string>
   <string name="wait_a_moment">Подождите немного</string>
@@ -236,20 +236,20 @@
   <string name="ssl_validator_label_signature">Подпись:</string>
   <string name="ssl_validator_label_signature_algorithm">Алгоритм:</string>
   <string name="ssl_validator_null_cert">Сертификат не может быть показан.</string>
-  <string name="ssl_validator_no_info_about_error">- Информации об ошибке нет</string>
+  <string name="ssl_validator_no_info_about_error">- Нет информации об ошибке</string>
   <string name="placeholder_sentence">Это заполнитель</string>
   <string name="placeholder_filename">placeholder.txt</string>
   <string name="placeholder_filetype">Изображение PNG</string>
   <string name="placeholder_filesize">389 КБ</string>
   <string name="placeholder_timestamp">2012/05/18 12:23 PM</string>
   <string name="placeholder_media_time">12:23:45</string>
-  <string name="instant_upload_on_wifi">Загружать изображения только через Wi-Fi</string>
+  <string name="instant_upload_on_wifi">Загрузка изображений только через Wi-Fi</string>
   <string name="instant_video_upload_on_wifi">Загрузка видео только через WiFi</string>
   <string name="instant_upload_path">/InstantUpload</string>
   <string name="conflict_title">Конфликт обновления</string>
   <string name="conflict_message">Удаленный файл %s не синхронизирован с локальным. Продолжение приведет к замене содержимого файла на сервере.</string>
   <string name="conflict_keep_both">Сохранить оба</string>
-  <string name="conflict_overwrite">Заменить</string>
+  <string name="conflict_overwrite">Перезаписать</string>
   <string name="conflict_dont_upload">Не загружать</string>
   <string name="preview_image_description">Предпросмотр</string>
   <string name="preview_image_error_unknown_format">Это изображение не может быть отображено</string>
@@ -299,6 +299,7 @@
   <string name="prefs_category_instant_uploading">Мгновенные загрузки</string>
   <string name="prefs_category_security">Безопасность</string>
   <string name="prefs_instant_video_upload_path_title">Путь для загрузки Видео</string>
+  <string name="download_folder_failed_content">Загрузка папки %1$s не может быть завершена</string>
   <string name="shared_subject_header">общие</string>
   <string name="with_you_subject_header">с вами</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 7 - 0
res/values-sk-rSK/strings.xml

@@ -282,6 +282,9 @@
   <string name="auth_redirect_non_secure_connection_title">Zabezpečené pripojenie je presmerované na nezabezpečenú trasu.</string>
   <string name="actionbar_logger">Logy</string>
   <string name="log_send_history_button">Odoslať históriu</string>
+  <string name="log_send_no_mail_app">Nebola nájdená aplikácia pre odosielanie log protokolov. Nainštalujte si mailovú aplikáciu!</string>
+  <string name="log_send_mail_subject">%1$s Android app logs</string>
+  <string name="log_progress_dialog_text">Načítavam dáta...</string>
   <string name="saml_authentication_required_text">Vyžaduje sa overenie</string>
   <string name="saml_authentication_wrong_pass">Nesprávne heslo</string>
   <string name="actionbar_move">Presunúť</string>
@@ -294,5 +297,9 @@
   <string name="forbidden_permissions_move">pre presun tohoto súboru</string>
   <string name="prefs_category_instant_uploading">Okamžité nahratie</string>
   <string name="prefs_category_security">Zabezpečenie</string>
+  <string name="prefs_instant_video_upload_path_title">Cesta pre nahrávanie videí</string>
+  <string name="download_folder_failed_content">Sťahovanie %1$s priečinka nebolo dokončené</string>
   <string name="shared_subject_header">zdieľané</string>
+  <string name="with_you_subject_header">s vami</string>
+  <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>
 </resources>

+ 1 - 0
res/values-tr/strings.xml

@@ -298,6 +298,7 @@
   <string name="prefs_category_instant_uploading">Anında Yüklemeler</string>
   <string name="prefs_category_security">Güvenlik</string>
   <string name="prefs_instant_video_upload_path_title">Video Yükleme Yolu</string>
+  <string name="download_folder_failed_content">%1$s klasörün indirilmesi tamamlanamadı</string>
   <string name="shared_subject_header">paylaşılan</string>
   <string name="with_you_subject_header">sizinle</string>
   <string name="subject_token">%1$s %2$s &gt;&gt;%3$s&lt;&lt; %4$s</string>

+ 1 - 0
res/values-zh-rCN/strings.xml

@@ -297,5 +297,6 @@
   <string name="prefs_category_instant_uploading">即时上传</string>
   <string name="prefs_category_security">安全</string>
   <string name="prefs_instant_video_upload_path_title">视频上传路径</string>
+  <string name="download_folder_failed_content">%1$s 文件夹的下载无法完成</string>
   <string name="shared_subject_header">分享</string>
 </resources>

+ 1 - 0
res/values/strings.xml

@@ -324,6 +324,7 @@
 	<string name="prefs_category_security">Security</string>
 
 	<string name="prefs_instant_video_upload_path_title">Upload Video Path</string>
+    <string name="download_folder_failed_content">Download of %1$s folder could not be completed</string>
 
 	<string name="shared_subject_header">shared</string>
 	<string name="with_you_subject_header">with you</string>

+ 10 - 12
res/xml/preferences.xml

@@ -31,25 +31,23 @@
                         android:summary="@string/prefs_pincode_summary"/>
 	</PreferenceCategory>
 
-    <PreferenceCategory android:title="@string/prefs_category_instant_uploading">
-        <com.owncloud.android.ui.PreferenceWithLongSummary
-							android:title="@string/prefs_instant_upload_path_title"
-							android:key="instant_upload_path" />
-	    <com.owncloud.android.ui.CheckBoxPreferenceWithLongTitle android:key="instant_uploading"
+    <PreferenceCategory android:title="@string/prefs_category_instant_uploading" android:key="instant_uploading_category">
+         <com.owncloud.android.ui.CheckBoxPreferenceWithLongTitle android:key="instant_uploading"
 	                        android:title="@string/prefs_instant_upload"
 	                        android:summary="@string/prefs_instant_upload_summary"/>
-	    <com.owncloud.android.ui.CheckBoxPreferenceWithLongTitle android:dependency="instant_uploading"
-	        				android:disableDependentsState="true"
+         <com.owncloud.android.ui.PreferenceWithLongSummary
+							android:title="@string/prefs_instant_upload_path_title"
+							android:key="instant_upload_path" />
+	    <com.owncloud.android.ui.CheckBoxPreferenceWithLongTitle
 	        				android:title="@string/instant_upload_on_wifi"
 	        				android:key="instant_upload_on_wifi"/>
+	    <com.owncloud.android.ui.CheckBoxPreferenceWithLongTitle android:key="instant_video_uploading"
+	                        android:title="@string/prefs_instant_video_upload"
+	                        android:summary="@string/prefs_instant_video_upload_summary" />
 	    <com.owncloud.android.ui.PreferenceWithLongSummary
 							android:title="@string/prefs_instant_video_upload_path_title"
 							android:key="instant_video_upload_path" />
-	    <com.owncloud.android.ui.CheckBoxPreferenceWithLongTitle android:key="instant_video_uploading"
-	                        android:title="@string/prefs_instant_video_upload"
-	                        android:summary="@string/prefs_instant_video_upload_summary"/>
-	    <com.owncloud.android.ui.CheckBoxPreferenceWithLongTitle android:dependency="instant_video_uploading"
-	        				android:disableDependentsState="true"
+	    <com.owncloud.android.ui.CheckBoxPreferenceWithLongTitle
 	        				android:title="@string/instant_video_upload_on_wifi"
 	        				android:key="instant_video_upload_on_wifi"/>
 	    <!-- DISABLED FOR RELEASE UNTIL FIXED

+ 4 - 4
src/com/owncloud/android/authentication/AuthenticatorActivity.java

@@ -683,7 +683,7 @@ SsoWebViewClientListener, OnSslUntrustedCertListener {
         
         if (mOperationsServiceBinder != null) {
             //Log_OC.wtf(TAG, "getting access token..." );
-            mWaitingForOpId = mOperationsServiceBinder.newOperation(getServerInfoIntent);
+            mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getServerInfoIntent);
         }
     }
 
@@ -752,7 +752,7 @@ SsoWebViewClientListener, OnSslUntrustedCertListener {
                 normalizeUrlSuffix(uri)
             );
             if (mOperationsServiceBinder != null) {
-                mWaitingForOpId = mOperationsServiceBinder.newOperation(getServerInfoIntent);
+                mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getServerInfoIntent);
             } else {
               Log_OC.wtf(TAG, "Server check tried with OperationService unbound!" );
             }
@@ -888,7 +888,7 @@ SsoWebViewClientListener, OnSslUntrustedCertListener {
         
         if (mOperationsServiceBinder != null) {
             //Log_OC.wtf(TAG, "starting existenceCheckRemoteOperation..." );
-            mWaitingForOpId = mOperationsServiceBinder.newOperation(existenceCheckIntent);
+            mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(existenceCheckIntent);
         }
     }
 
@@ -1704,7 +1704,7 @@ SsoWebViewClientListener, OnSslUntrustedCertListener {
         
         if (mOperationsServiceBinder != null) {
             //Log_OC.wtf(TAG, "starting getRemoteUserNameOperation..." );
-            mWaitingForOpId = mOperationsServiceBinder.newOperation(getUserNameIntent);
+            mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getUserNameIntent);
         }
     }
 

+ 59 - 8
src/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -46,6 +46,7 @@ import android.content.OperationApplicationException;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.RemoteException;
+import android.provider.MediaStore;
 
 public class FileDataStorageManager {
 
@@ -193,6 +194,7 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.FILE_PERMISSIONS, file.getPermissions());
         cv.put(ProviderTableMeta.FILE_REMOTE_ID, file.getRemoteId());
         cv.put(ProviderTableMeta.FILE_UPDATE_THUMBNAIL, file.needsUpdateThumbnail());
+        cv.put(ProviderTableMeta.FILE_IS_DOWNLOADING, file.isDownloading());
         
         boolean sameRemotePath = fileExists(file.getRemotePath());
         if (sameRemotePath ||
@@ -261,8 +263,8 @@ public class FileDataStorageManager {
      * HERE ONLY DATA CONSISTENCY SHOULD BE GRANTED
      *  
      * @param folder
-     * @param files
-     * @param removeNotUpdated
+     * @param updatedFiles
+     * @param filesToRemove
      */
     public void saveFolder(
             OCFile folder, Collection<OCFile> updatedFiles, Collection<OCFile> filesToRemove
@@ -302,6 +304,7 @@ public class FileDataStorageManager {
             cv.put(ProviderTableMeta.FILE_PERMISSIONS, file.getPermissions());
             cv.put(ProviderTableMeta.FILE_REMOTE_ID, file.getRemoteId());
             cv.put(ProviderTableMeta.FILE_UPDATE_THUMBNAIL, file.needsUpdateThumbnail());
+            cv.put(ProviderTableMeta.FILE_IS_DOWNLOADING, file.isDownloading());
 
             boolean existsByPath = fileExists(file.getRemotePath());
             if (existsByPath || fileExists(file.getFileId())) {
@@ -491,7 +494,7 @@ public class FileDataStorageManager {
                 if (removeLocalCopy && file.isDown() && localPath != null && success) {
                     success = new File(localPath).delete();
                     if (success) {
-                        triggerMediaScan(localPath);
+                        deleteFileInMediaScan(localPath);
                     }
                     if (!removeDBData && success) {
                         // maybe unnecessary, but should be checked TODO remove if unnecessary
@@ -539,7 +542,8 @@ public class FileDataStorageManager {
 
     private boolean removeLocalFolder(OCFile folder) {
         boolean success = true;
-        File localFolder = new File(FileStorageUtils.getDefaultSavePathFor(mAccount.name, folder));
+        String localFolderPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, folder);
+        File localFolder = new File(localFolderPath);
         if (localFolder.exists()) {
             // stage 1: remove the local files already registered in the files database
             Vector<OCFile> files = getFolderContent(folder.getFileId());
@@ -549,13 +553,13 @@ public class FileDataStorageManager {
                         success &= removeLocalFolder(file);
                     } else {
                         if (file.isDown()) {
-                            String path = file.getStoragePath();
                             File localFile = new File(file.getStoragePath());
                             success &= localFile.delete();
                             if (success) {
+                                // notify MediaScanner about removed file
+                                deleteFileInMediaScan(file.getStoragePath());
                                 file.setStoragePath(null);
                                 saveFile(file);
-                                triggerMediaScan(path); // notify MediaScanner about removed file
                             }
                         }
                     }
@@ -579,7 +583,6 @@ public class FileDataStorageManager {
                 } else {
                     String path = localFile.getAbsolutePath();
                     success &= localFile.delete();
-                    triggerMediaScan(path); // notify MediaScanner about removed file
                 }
             }
         }
@@ -714,7 +717,7 @@ public class FileDataStorageManager {
                 Iterator<String> it = originalPathsToTriggerMediaScan.iterator();
                 while (it.hasNext()) {
                     // Notify MediaScanner about removed file
-                    triggerMediaScan(it.next());
+                    deleteFileInMediaScan(it.next());
                 }
                 it = newPathsToTriggerMediaScan.iterator();
                 while (it.hasNext()) {
@@ -877,6 +880,8 @@ public class FileDataStorageManager {
             file.setRemoteId(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_REMOTE_ID)));
             file.setNeedsUpdateThumbnail(c.getInt(
                     c.getColumnIndex(ProviderTableMeta.FILE_UPDATE_THUMBNAIL)) == 1 ? true : false);
+            file.setDownloading(c.getInt(
+                    c.getColumnIndex(ProviderTableMeta.FILE_IS_DOWNLOADING)) == 1 ? true : false);
                     
         }
         return file;
@@ -1259,6 +1264,10 @@ public class FileDataStorageManager {
                     ProviderTableMeta.FILE_UPDATE_THUMBNAIL, 
                     file.needsUpdateThumbnail() ? 1 : 0
                 );
+                cv.put(
+                        ProviderTableMeta.FILE_IS_DOWNLOADING,
+                        file.isDownloading() ? 1 : 0
+                );
 
                 boolean existsByPath = fileExists(file.getRemotePath());
                 if (existsByPath || fileExists(file.getFileId())) {
@@ -1490,4 +1499,46 @@ public class FileDataStorageManager {
         MainApp.getAppContext().sendBroadcast(intent);
     }
 
+    public void deleteFileInMediaScan(String path) {
+
+        String mimetypeString = FileStorageUtils.getMimeTypeFromName(path);
+        ContentResolver contentResolver = getContentResolver();
+
+        if (contentResolver != null) {
+            if (mimetypeString.startsWith("image/")) {
+                // Images
+                contentResolver.delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                        MediaStore.Images.Media.DATA + "=?", new String[]{path});
+            } else if (mimetypeString.startsWith("audio/")) {
+                // Audio
+                contentResolver.delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                        MediaStore.Audio.Media.DATA + "=?", new String[]{path});
+            } else if (mimetypeString.startsWith("video/")) {
+                // Video
+                contentResolver.delete(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+                        MediaStore.Video.Media.DATA + "=?", new String[]{path});
+            }
+        } else {
+            ContentProviderClient contentProviderClient = getContentProviderClient();
+            try {
+                if (mimetypeString.startsWith("image/")) {
+                    // Images
+                    contentProviderClient.delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                            MediaStore.Images.Media.DATA + "=?", new String[]{path});
+                } else if (mimetypeString.startsWith("audio/")) {
+                    // Audio
+                    contentProviderClient.delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                            MediaStore.Audio.Media.DATA + "=?", new String[]{path});
+                } else if (mimetypeString.startsWith("video/")) {
+                    // Video
+                    contentProviderClient.delete(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+                            MediaStore.Video.Media.DATA + "=?", new String[]{path});
+                }
+            } catch (RemoteException e) {
+                Log_OC.e(TAG, "Exception deleting media file in MediaStore " + e.getMessage());
+            }
+        }
+
+    }
+
 }

+ 19 - 12
src/com/owncloud/android/datamodel/OCFile.java

@@ -20,9 +20,9 @@ package com.owncloud.android.datamodel;
 
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.webkit.MimeTypeMap;
 
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.utils.FileStorageUtils;
 
 import java.io.File;
 
@@ -70,6 +70,8 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
 
     private boolean mNeedsUpdateThumbnail;
 
+    private boolean mIsDownloading;
+
 
     /**
      * Create new {@link OCFile} with given path.
@@ -112,6 +114,7 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
         mPermissions = source.readString();
         mRemoteId = source.readString();
         mNeedsUpdateThumbnail = source.readInt() == 0;
+        mIsDownloading = source.readInt() == 0;
 
     }
 
@@ -136,6 +139,7 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
         dest.writeString(mPermissions);
         dest.writeString(mRemoteId);
         dest.writeInt(mNeedsUpdateThumbnail ? 1 : 0);
+        dest.writeInt(mIsDownloading ? 1 : 0);
     }
 
     /**
@@ -348,6 +352,7 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
         mPermissions = null;
         mRemoteId = null;
         mNeedsUpdateThumbnail = false;
+        mIsDownloading = false;
     }
 
     /**
@@ -533,17 +538,7 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
      */
     public boolean isImage() {
         return ((mMimeType != null && mMimeType.startsWith("image/")) ||
-                getMimeTypeFromName().startsWith("image/"));
-    }
-
-    public String getMimeTypeFromName() {
-        String extension = "";
-        int pos = mRemotePath.lastIndexOf('.');
-        if (pos >= 0) {
-            extension = mRemotePath.substring(pos + 1);
-        }
-        String result = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase());
-        return (result != null) ? result : "";
+                FileStorageUtils.getMimeTypeFromName(mRemotePath).startsWith("image/"));
     }
 
     public String getPermissions() {
@@ -562,4 +557,16 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
         this.mRemoteId = remoteId;
     }
 
+    public boolean isDownloading() {
+        return mIsDownloading;
+    }
+
+    public void setDownloading(boolean isDownloading) {
+        this.mIsDownloading = isDownloading;
+    }
+
+    public boolean isSynchronizing() {
+        // TODO real implementation
+        return false;
+    }
 }

+ 2 - 1
src/com/owncloud/android/db/ProviderMeta.java

@@ -31,7 +31,7 @@ import com.owncloud.android.MainApp;
 public class ProviderMeta {
 
     public static final String DB_NAME = "filelist";
-    public static final int DB_VERSION = 8;
+    public static final int DB_VERSION = 9;
 
     private ProviderMeta() {
     }
@@ -71,6 +71,7 @@ public class ProviderMeta {
         public static final String FILE_PERMISSIONS = "permissions";
         public static final String FILE_REMOTE_ID = "remote_id";
         public static final String FILE_UPDATE_THUMBNAIL = "update_thumbnail";
+        public static final String FILE_IS_DOWNLOADING= "is_downloading";
 
         public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME
                 + " collate nocase asc";

+ 10 - 6
src/com/owncloud/android/files/FileMenuFilter.java

@@ -31,6 +31,8 @@ import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
+import com.owncloud.android.services.OperationsService;
+import com.owncloud.android.services.OperationsService.OperationsServiceBinder;
 import com.owncloud.android.ui.activity.ComponentsGetter;
 
 /**
@@ -51,8 +53,8 @@ public class FileMenuFilter {
      * 
      * @param targetFile        {@link OCFile} target of the action to filter in the {@link Menu}.
      * @param account           ownCloud {@link Account} holding targetFile.
-     * @param cg                Accessor to app components, needed to get access the 
-     *                          {@link FileUploader} and {@link FileDownloader} services.
+     * @param cg                Accessor to app components, needed to access the
+     *                          {@link FileUploader} and {@link FileDownloader} services
      * @param context           Android {@link Context}, needed to access build setup resources.
      */
     public FileMenuFilter(OCFile targetFile, Account account, ComponentsGetter cg, Context context) {
@@ -140,15 +142,17 @@ public class FileMenuFilter {
         boolean uploading = false;
         if (mComponentsGetter != null && mFile != null && mAccount != null) {
             FileDownloaderBinder downloaderBinder = mComponentsGetter.getFileDownloaderBinder();
-            downloading = downloaderBinder != null && downloaderBinder.isDownloading(mAccount, mFile);
+            downloading = (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, mFile));
+            OperationsServiceBinder opsBinder = mComponentsGetter.getOperationsServiceBinder();
+            downloading |= (opsBinder != null && opsBinder.isSynchronizing(mAccount, mFile.getRemotePath()));
             FileUploaderBinder uploaderBinder = mComponentsGetter.getFileUploaderBinder();
-            uploading = uploaderBinder != null && uploaderBinder.isUploading(mAccount, mFile);
+            uploading = (uploaderBinder != null && uploaderBinder.isUploading(mAccount, mFile));
         }
         
         /// decision is taken for each possible action on a file in the menu
         
         // DOWNLOAD 
-        if (mFile == null || mFile.isFolder() || mFile.isDown() || downloading || uploading) {
+        if (mFile == null || mFile.isDown() || downloading || uploading) {
             toHide.add(R.id.action_download_file);
             
         } else {
@@ -189,7 +193,7 @@ public class FileMenuFilter {
         
         
         // CANCEL DOWNLOAD
-        if (mFile == null || !downloading || mFile.isFolder()) {
+        if (mFile == null || !downloading) {
             toHide.add(R.id.action_cancel_download);
         } else {
             toShow.add(R.id.action_cancel_download);

+ 43 - 24
src/com/owncloud/android/files/FileOperationsHelper.java

@@ -1,5 +1,5 @@
 /* ownCloud Android client application
- *   Copyright (C) 2012-2014 ownCloud Inc.
+ *   Copyright (C) 2012-2015 ownCloud Inc.
  *
  *   This program is free software: you can redistribute it and/or modify
  *   it under the terms of the GNU General Public License version 2,
@@ -127,7 +127,7 @@ public class FileOperationsHelper {
             service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
             service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
             service.putExtra(OperationsService.EXTRA_SEND_INTENT, sendIntent);
-            mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service);
+            mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service);
             
         } else {
             Log_OC.wtf(TAG, "Trying to open a NULL OCFile");
@@ -165,7 +165,7 @@ public class FileOperationsHelper {
             service.setAction(OperationsService.ACTION_UNSHARE);
             service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
             service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
-            mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service);
+            mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service);
             
             mFileActivity.showLoadingDialog();
             
@@ -197,18 +197,25 @@ public class FileOperationsHelper {
     
     
     public void syncFile(OCFile file) {
-        // Sync file
-        Intent service = new Intent(mFileActivity, OperationsService.class);
-        service.setAction(OperationsService.ACTION_SYNC_FILE);
-        service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
-        service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); 
-        service.putExtra(OperationsService.EXTRA_SYNC_FILE_CONTENTS, true);
-        mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service);
         
-        mFileActivity.showLoadingDialog();
+        if (!file.isFolder()){
+            Intent intent = new Intent(mFileActivity, OperationsService.class);
+            intent.setAction(OperationsService.ACTION_SYNC_FILE);
+            intent.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
+            intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
+            intent.putExtra(OperationsService.EXTRA_SYNC_FILE_CONTENTS, true);
+            mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(intent);
+            mFileActivity.showLoadingDialog();
+            
+        } else {
+            Intent intent = new Intent(mFileActivity, OperationsService.class);
+            intent.setAction(OperationsService.ACTION_SYNC_FOLDER);
+            intent.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
+            intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
+            mFileActivity.startService(intent);
+        }
     }
     
-    
     public void renameFile(OCFile file, String newFilename) {
         // RenameFile
         Intent service = new Intent(mFileActivity, OperationsService.class);
@@ -216,7 +223,7 @@ public class FileOperationsHelper {
         service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
         service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
         service.putExtra(OperationsService.EXTRA_NEWNAME, newFilename);
-        mWaitingForOpId = mFileActivity.getOperationsServiceBinder().newOperation(service);
+        mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service);
         
         mFileActivity.showLoadingDialog();
     }
@@ -229,7 +236,7 @@ public class FileOperationsHelper {
         service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
         service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
         service.putExtra(OperationsService.EXTRA_REMOVE_ONLY_LOCAL, onlyLocalCopy);
-        mWaitingForOpId =  mFileActivity.getOperationsServiceBinder().newOperation(service);
+        mWaitingForOpId =  mFileActivity.getOperationsServiceBinder().queueNewOperation(service);
         
         mFileActivity.showLoadingDialog();
     }
@@ -242,26 +249,38 @@ public class FileOperationsHelper {
         service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
         service.putExtra(OperationsService.EXTRA_REMOTE_PATH, remotePath);
         service.putExtra(OperationsService.EXTRA_CREATE_FULL_PATH, createFullPath);
-        mWaitingForOpId =  mFileActivity.getOperationsServiceBinder().newOperation(service);
+        mWaitingForOpId =  mFileActivity.getOperationsServiceBinder().queueNewOperation(service);
         
         mFileActivity.showLoadingDialog();
     }
 
-    
+    /**
+     * Cancel the transference in downloads (files/folders) and file uploads
+     * @param file OCFile
+     */
     public void cancelTransference(OCFile file) {
         Account account = mFileActivity.getAccount();
+        if (file.isFolder()) {
+            OperationsService.OperationsServiceBinder opsBinder = mFileActivity.getOperationsServiceBinder();
+            if (opsBinder != null) {
+                opsBinder.cancel(account, file);
+            }
+        }
+
+        // for both files and folders
         FileDownloaderBinder downloaderBinder = mFileActivity.getFileDownloaderBinder();
-        FileUploaderBinder uploaderBinder =  mFileActivity.getFileUploaderBinder();
+        FileUploaderBinder uploaderBinder = mFileActivity.getFileUploaderBinder();
         if (downloaderBinder != null && downloaderBinder.isDownloading(account, file)) {
+            downloaderBinder.cancel(account, file);
+
+            // TODO - review why is this here, and solve in a better way
             // Remove etag for parent, if file is a keep_in_sync
             if (file.keepInSync()) {
-               OCFile parent = mFileActivity.getStorageManager().getFileById(file.getParentId());
-               parent.setEtag("");
-               mFileActivity.getStorageManager().saveFile(parent);
+                OCFile parent = mFileActivity.getStorageManager().getFileById(file.getParentId());
+                parent.setEtag("");
+                mFileActivity.getStorageManager().saveFile(parent);
             }
-            
-            downloaderBinder.cancel(account, file);
-            
+
         } else if (uploaderBinder != null && uploaderBinder.isUploading(account, file)) {
             uploaderBinder.cancel(account, file);
         }
@@ -279,7 +298,7 @@ public class FileOperationsHelper {
         service.putExtra(OperationsService.EXTRA_NEW_PARENT_PATH, newfile.getRemotePath());
         service.putExtra(OperationsService.EXTRA_REMOTE_PATH, currentFile.getRemotePath());
         service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount());
-        mWaitingForOpId =  mFileActivity.getOperationsServiceBinder().newOperation(service);
+        mWaitingForOpId =  mFileActivity.getOperationsServiceBinder().queueNewOperation(service);
 
         mFileActivity.showLoadingDialog();
     }

+ 181 - 127
src/com/owncloud/android/files/services/FileDownloader.java

@@ -1,6 +1,6 @@
 /* ownCloud Android client application
  *   Copyright (C) 2012 Bartek Przybylski
- *   Copyright (C) 2012-2013 ownCloud Inc.
+ *   Copyright (C) 2012-2015 ownCloud Inc.
  *
  *   This program is free software: you can redistribute it and/or modify
  *   it under the terms of the GNU General Public License version 2,
@@ -25,8 +25,6 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Vector;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
 
 import com.owncloud.android.R;
 import com.owncloud.android.authentication.AuthenticatorActivity;
@@ -64,17 +62,19 @@ import android.os.Looper;
 import android.os.Message;
 import android.os.Process;
 import android.support.v4.app.NotificationCompat;
+import android.util.Pair;
 
 public class FileDownloader extends Service implements OnDatatransferProgressListener {
     
     public static final String EXTRA_ACCOUNT = "ACCOUNT";
     public static final String EXTRA_FILE = "FILE";
-    
+
     private static final String DOWNLOAD_ADDED_MESSAGE = "DOWNLOAD_ADDED";
     private static final String DOWNLOAD_FINISH_MESSAGE = "DOWNLOAD_FINISH";
     public static final String EXTRA_DOWNLOAD_RESULT = "RESULT";    
     public static final String EXTRA_FILE_PATH = "FILE_PATH";
     public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH";
+    public static final String EXTRA_LINKED_TO_PATH = "LINKED_TO";
     public static final String ACCOUNT_NAME = "ACCOUNT_NAME";
     
     private static final String TAG = "FileDownloader";
@@ -83,35 +83,25 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
     private ServiceHandler mServiceHandler;
     private IBinder mBinder;
     private OwnCloudClient mDownloadClient = null;
-    private Account mLastAccount = null;
+    private Account mCurrentAccount = null;
     private FileDataStorageManager mStorageManager;
     
-    private ConcurrentMap<String, DownloadFileOperation> mPendingDownloads = new ConcurrentHashMap<String, DownloadFileOperation>();
+    private IndexedForest<DownloadFileOperation> mPendingDownloads = new IndexedForest<DownloadFileOperation>();
+
     private DownloadFileOperation mCurrentDownload = null;
     
     private NotificationManager mNotificationManager;
     private NotificationCompat.Builder mNotificationBuilder;
     private int mLastPercent;
-    
+
     
     public static String getDownloadAddedMessage() {
-        return FileDownloader.class.getName().toString() + DOWNLOAD_ADDED_MESSAGE;
+        return FileDownloader.class.getName() + DOWNLOAD_ADDED_MESSAGE;
     }
     
     public static String getDownloadFinishMessage() {
-        return FileDownloader.class.getName().toString() + DOWNLOAD_FINISH_MESSAGE;
-    }
-    
-    /**
-     * Builds a key for mPendingDownloads from the account and file to download
-     * 
-     * @param account   Account where the file to download is stored
-     * @param file      File to download
-     */
-    private String buildRemoteName(Account account, OCFile file) {
-        return account.name + file.getRemotePath();
+        return FileDownloader.class.getName() + DOWNLOAD_FINISH_MESSAGE;
     }
-
     
     /**
      * Service initialization
@@ -130,52 +120,73 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
 
     /**
      * Entry point to add one or several files to the queue of downloads.
-     * 
-     * New downloads are added calling to startService(), resulting in a call to this method. This ensures the service will keep on working 
-     * although the caller activity goes away.
+     *
+     * New downloads are added calling to startService(), resulting in a call to this method.
+     * This ensures the service will keep on working although the caller activity goes away.
      */
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         if (    !intent.hasExtra(EXTRA_ACCOUNT) ||
                 !intent.hasExtra(EXTRA_FILE)
-                /*!intent.hasExtra(EXTRA_FILE_PATH) ||
-                !intent.hasExtra(EXTRA_REMOTE_PATH)*/
            ) {
             Log_OC.e(TAG, "Not enough information provided in intent");
             return START_NOT_STICKY;
-        }
-        Account account = intent.getParcelableExtra(EXTRA_ACCOUNT);
-        OCFile file = intent.getParcelableExtra(EXTRA_FILE);
-        
-        AbstractList<String> requestedDownloads = new Vector<String>(); // dvelasco: now this always contains just one element, but that can change in a near future (download of multiple selection)
-        String downloadKey = buildRemoteName(account, file);
-        try {
-            DownloadFileOperation newDownload = new DownloadFileOperation(account, file); 
-            mPendingDownloads.putIfAbsent(downloadKey, newDownload);
-            newDownload.addDatatransferProgressListener(this);
-            newDownload.addDatatransferProgressListener((FileDownloaderBinder)mBinder);
-            requestedDownloads.add(downloadKey);
-            sendBroadcastNewDownload(newDownload);
-            
-        } catch (IllegalArgumentException e) {
-            Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage());
-            return START_NOT_STICKY;
-        }
-        
-        if (requestedDownloads.size() > 0) {
-            Message msg = mServiceHandler.obtainMessage();
-            msg.arg1 = startId;
-            msg.obj = requestedDownloads;
-            mServiceHandler.sendMessage(msg);
+        } else {
+            final Account account = intent.getParcelableExtra(EXTRA_ACCOUNT);
+            final OCFile file = intent.getParcelableExtra(EXTRA_FILE);
+
+            /*Log_OC.v(
+                    "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                    "Received request to download file"
+            );*/
+
+                AbstractList<String> requestedDownloads = new Vector<String>();
+                try {
+                    DownloadFileOperation newDownload = new DownloadFileOperation(account, file);
+                    newDownload.addDatatransferProgressListener(this);
+                    newDownload.addDatatransferProgressListener((FileDownloaderBinder) mBinder);
+                    Pair<String, String> putResult = mPendingDownloads.putIfAbsent(
+                        account, file.getRemotePath(), newDownload
+                    );
+                    String downloadKey = putResult.first;
+                    requestedDownloads.add(downloadKey);
+                    /*Log_OC.v(
+                        "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                        "Download on " + file.getRemotePath() + " added to queue"
+                    );*/
+
+                    // Store file on db with state 'downloading'
+                    /*
+                    TODO - check if helps with UI responsiveness, letting only folders use FileDownloaderBinder to check
+                    FileDataStorageManager storageManager = new FileDataStorageManager(account, getContentResolver());
+                    file.setDownloading(true);
+                    storageManager.saveFile(file);
+                    */
+
+                    sendBroadcastNewDownload(newDownload, putResult.second);
+
+                } catch (IllegalArgumentException e) {
+                    Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage());
+                    return START_NOT_STICKY;
+                }
+
+                if (requestedDownloads.size() > 0) {
+                    Message msg = mServiceHandler.obtainMessage();
+                    msg.arg1 = startId;
+                    msg.obj = requestedDownloads;
+                    mServiceHandler.sendMessage(msg);
+                }
+            //}
         }
 
         return START_NOT_STICKY;
     }
-    
-    
+
+
     /**
-     * Provides a binder object that clients can use to perform operations on the queue of downloads, excepting the addition of new files. 
-     * 
+     * Provides a binder object that clients can use to perform operations on the queue of downloads,
+     * excepting the addition of new files.
+     *
      * Implemented to perform cancellation, pause and resume of existing downloads.
      */
     @Override
@@ -193,33 +204,49 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
         return false;   // not accepting rebinding (default behaviour)
     }
 
-    
+
     /**
      *  Binder to let client components to perform operations on the queue of downloads.
-     * 
+     *
      *  It provides by itself the available operations.
      */
     public class FileDownloaderBinder extends Binder implements OnDatatransferProgressListener {
         
         /** 
-         * Map of listeners that will be reported about progress of downloads from a {@link FileDownloaderBinder} instance 
+         * Map of listeners that will be reported about progress of downloads from a {@link FileDownloaderBinder}
+         * instance.
          */
-        private Map<String, OnDatatransferProgressListener> mBoundListeners = new HashMap<String, OnDatatransferProgressListener>();
-        
-        
+        private Map<Long, OnDatatransferProgressListener> mBoundListeners =
+                new HashMap<Long, OnDatatransferProgressListener>();
+
+
         /**
          * Cancels a pending or current download of a remote file.
-         * 
-         * @param account       Owncloud account where the remote file is stored.
+         *
+         * @param account       ownCloud account where the remote file is stored.
          * @param file          A file in the queue of pending downloads
          */
         public void cancel(Account account, OCFile file) {
-            DownloadFileOperation download = null;
-            synchronized (mPendingDownloads) {
-                download = mPendingDownloads.remove(buildRemoteName(account, file));
-            }
+            /*Log_OC.v(
+                    "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                    "Received request to cancel download of " + file.getRemotePath()
+            );
+            Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                    "Removing download of " + file.getRemotePath());*/
+            Pair<DownloadFileOperation, String> removeResult = mPendingDownloads.remove(account, file.getRemotePath());
+            DownloadFileOperation download = removeResult.first;
             if (download != null) {
+                /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                        "Canceling returned download of " + file.getRemotePath());*/
                 download.cancel();
+            } else {
+                if (mCurrentDownload != null && mCurrentAccount != null &&
+                        mCurrentDownload.getRemotePath().startsWith(file.getRemotePath()) &&
+                        account.name.equals(mCurrentAccount.name)) {
+                    /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                            "Canceling current sync as descendant: " + mCurrentDownload.getRemotePath());*/
+                    mCurrentDownload.cancel();
+                }
             }
         }
         
@@ -230,29 +257,18 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
 
 
         /**
-         * Returns True when the file described by 'file' in the ownCloud account 'account' is downloading or waiting to download.
+         * Returns True when the file described by 'file' in the ownCloud account 'account' is downloading or
+         * waiting to download.
          * 
-         * If 'file' is a directory, returns 'true' if some of its descendant files is downloading or waiting to download. 
+         * If 'file' is a directory, returns 'true' if any of its descendant files is downloading or
+         * waiting to download.
          * 
-         * @param account       Owncloud account where the remote file is stored.
+         * @param account       ownCloud account where the remote file is stored.
          * @param file          A file that could be in the queue of downloads.
          */
         public boolean isDownloading(Account account, OCFile file) {
             if (account == null || file == null) return false;
-            String targetKey = buildRemoteName(account, file);
-            synchronized (mPendingDownloads) {
-                if (file.isFolder()) {
-                    // this can be slow if there are many downloads :(
-                    Iterator<String> it = mPendingDownloads.keySet().iterator();
-                    boolean found = false;
-                    while (it.hasNext() && !found) {
-                        found = it.next().startsWith(targetKey);
-                    }
-                    return found;
-                } else {
-                    return (mPendingDownloads.containsKey(targetKey));
-                }
-            }
+            return (mPendingDownloads.contains(account, file.getRemotePath()));
         }
 
         
@@ -261,12 +277,14 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
          * 
          * @param listener      Object to notify about progress of transfer.    
          * @param account       ownCloud account holding the file of interest.
-         * @param file          {@link OCfile} of interest for listener. 
+         * @param file          {@link OCFile} of interest for listener.
          */
-        public void addDatatransferProgressListener (OnDatatransferProgressListener listener, Account account, OCFile file) {
+        public void addDatatransferProgressListener (
+                OnDatatransferProgressListener listener, Account account, OCFile file
+        ) {
             if (account == null || file == null || listener == null) return;
-            String targetKey = buildRemoteName(account, file);
-            mBoundListeners.put(targetKey, listener);
+            //String targetKey = buildKey(account, file.getRemotePath());
+            mBoundListeners.put(file.getFileId(), listener);
         }
         
         
@@ -275,21 +293,24 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
          * 
          * @param listener      Object to notify about progress of transfer.    
          * @param account       ownCloud account holding the file of interest.
-         * @param file          {@link OCfile} of interest for listener. 
+         * @param file          {@link OCFile} of interest for listener.
          */
-        public void removeDatatransferProgressListener (OnDatatransferProgressListener listener, Account account, OCFile file) {
+        public void removeDatatransferProgressListener (
+                OnDatatransferProgressListener listener, Account account, OCFile file
+        ) {
             if (account == null || file == null || listener == null) return;
-            String targetKey = buildRemoteName(account, file);
-            if (mBoundListeners.get(targetKey) == listener) {
-                mBoundListeners.remove(targetKey);
+            //String targetKey = buildKey(account, file.getRemotePath());
+            Long fileId = file.getFileId();
+            if (mBoundListeners.get(fileId) == listener) {
+                mBoundListeners.remove(fileId);
             }
         }
 
         @Override
         public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer,
                 String fileName) {
-            String key = buildRemoteName(mCurrentDownload.getAccount(), mCurrentDownload.getFile());
-            OnDatatransferProgressListener boundListener = mBoundListeners.get(key);
+            //String key = buildKey(mCurrentDownload.getAccount(), mCurrentDownload.getFile().getRemotePath());
+            OnDatatransferProgressListener boundListener = mBoundListeners.get(mCurrentDownload.getFile().getFileId());
             if (boundListener != null) {
                 boundListener.onTransferProgress(progressRate, totalTransferredSoFar, totalToTransfer, fileName);
             }
@@ -320,7 +341,10 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
             if (msg.obj != null) {
                 Iterator<String> it = requestedDownloads.iterator();
                 while (it.hasNext()) {
-                    mService.downloadFile(it.next());
+                    String next = it.next();
+                    /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                            "Handling download file " + next);*/
+                    mService.downloadFile(next);
                 }
             }
             mService.stopSelf(msg.arg1);
@@ -334,11 +358,11 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
      * @param downloadKey   Key to access the download to perform, contained in mPendingDownloads 
      */
     private void downloadFile(String downloadKey) {
-        
-        synchronized(mPendingDownloads) {
-            mCurrentDownload = mPendingDownloads.get(downloadKey);
-        }
-        
+
+        /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                "Getting download of " + downloadKey);*/
+        mCurrentDownload = mPendingDownloads.get(downloadKey);
+
         if (mCurrentDownload != null) {
             
             notifyDownloadStart(mCurrentDownload);
@@ -346,39 +370,48 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
             RemoteOperationResult downloadResult = null;
             try {
                 /// prepare client object to send the request to the ownCloud server
-                if (mDownloadClient == null || !mLastAccount.equals(mCurrentDownload.getAccount())) {
-                    mLastAccount = mCurrentDownload.getAccount();
-                    mStorageManager = 
-                            new FileDataStorageManager(mLastAccount, getContentResolver());
-                    OwnCloudAccount ocAccount = new OwnCloudAccount(mLastAccount, this);
-                    mDownloadClient = OwnCloudClientManagerFactory.getDefaultSingleton().
-                            getClientFor(ocAccount, this);
-                }
+                if (mCurrentAccount == null || !mCurrentAccount.equals(mCurrentDownload.getAccount())) {
+                    mCurrentAccount = mCurrentDownload.getAccount();
+                    mStorageManager = new FileDataStorageManager(
+                            mCurrentAccount,
+                            getContentResolver()
+                    );
+                }   // else, reuse storage manager from previous operation
+
+                // always get client from client manager, to get fresh credentials in case of update
+                OwnCloudAccount ocAccount = new OwnCloudAccount(mCurrentAccount, this);
+                mDownloadClient = OwnCloudClientManagerFactory.getDefaultSingleton().
+                        getClientFor(ocAccount, this);
+
 
                 /// perform the download
+                /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                        "Executing download of " + mCurrentDownload.getRemotePath());*/
                 downloadResult = mCurrentDownload.execute(mDownloadClient);
                 if (downloadResult.isSuccess()) {
                     saveDownloadedFile();
                 }
             
             } catch (AccountsException e) {
-                Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e);
+                Log_OC.e(TAG, "Error while trying to get authorization for " + mCurrentAccount.name, e);
                 downloadResult = new RemoteOperationResult(e);
             } catch (IOException e) {
-                Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e);
+                Log_OC.e(TAG, "Error while trying to get authorization for " + mCurrentAccount.name, e);
                 downloadResult = new RemoteOperationResult(e);
                 
             } finally {
-                synchronized(mPendingDownloads) {
-                    mPendingDownloads.remove(downloadKey);
-                }
+                /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                        "Removing payload " + mCurrentDownload.getRemotePath());*/
+
+                Pair<DownloadFileOperation, String> removeResult =
+                        mPendingDownloads.removePayload(mCurrentAccount, mCurrentDownload.getRemotePath());
+
+                /// notify result
+                notifyDownloadResult(mCurrentDownload, downloadResult);
+
+                sendBroadcastDownloadFinished(mCurrentDownload, downloadResult, removeResult.second);
             }
 
-            
-            /// notify result
-            notifyDownloadResult(mCurrentDownload, downloadResult);
-            
-            sendBroadcastDownloadFinished(mCurrentDownload, downloadResult);
         }
     }
 
@@ -403,6 +436,15 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
         mStorageManager.triggerMediaScan(file.getStoragePath());
     }
 
+    /**
+     * Update the OC File after a unsuccessful download
+     */
+    private void updateUnsuccessfulDownloadedFile() {
+        OCFile file = mStorageManager.getFileById(mCurrentDownload.getFile().getFileId());
+        file.setDownloading(false);
+        mStorageManager.saveFile(file);
+    }
+
 
     /**
      * Creates a status notification to show the download progress
@@ -448,7 +490,8 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
      * Callback method to update the progress bar in the status notification.
      */
     @Override
-    public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String filePath) {
+    public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String filePath)
+    {
         int percent = (int)(100.0*((double)totalTransferredSoFar)/((double)totalToTransfer));
         if (percent != mLastPercent) {
             mNotificationBuilder.setProgress(100, percent, totalToTransfer < 0);
@@ -492,7 +535,9 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
                 // let the user update credentials with one click
                 Intent updateAccountCredentials = new Intent(this, AuthenticatorActivity.class);
                 updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, download.getAccount());
-                updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACTION, AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN);
+                updateAccountCredentials.putExtra(
+                        AuthenticatorActivity.EXTRA_ACTION, AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN
+                );
                 updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                 updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
                 updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND);
@@ -500,8 +545,6 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
                     .setContentIntent(PendingIntent.getActivity(
                         this, (int) System.currentTimeMillis(), updateAccountCredentials, PendingIntent.FLAG_ONE_SHOT));
                 
-                mDownloadClient = null;   // grant that future retries on the same account will get the fresh credentials
-                
             } else {
                 // TODO put something smart in showDetailsIntent
                 Intent   showDetailsIntent = new Intent();
@@ -510,7 +553,9 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
                         this, (int) System.currentTimeMillis(), showDetailsIntent, 0));
             }
             
-            mNotificationBuilder.setContentText(ErrorMessageAdapter.getErrorCauseMessage(downloadResult, download, getResources()));
+            mNotificationBuilder.setContentText(
+                    ErrorMessageAdapter.getErrorCauseMessage(downloadResult, download, getResources())
+            );
             mNotificationManager.notify(tickerId, mNotificationBuilder.build());
             
             // Remove success notification
@@ -529,15 +574,22 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
     /**
      * Sends a broadcast when a download finishes in order to the interested activities can update their view
      * 
-     * @param download          Finished download operation
-     * @param downloadResult    Result of the download operation
+     * @param download                  Finished download operation
+     * @param downloadResult            Result of the download operation
+     * @param unlinkedFromRemotePath    Path in the downloads tree where the download was unlinked from
      */
-    private void sendBroadcastDownloadFinished(DownloadFileOperation download, RemoteOperationResult downloadResult) {
+    private void sendBroadcastDownloadFinished(
+            DownloadFileOperation download,
+            RemoteOperationResult downloadResult,
+            String unlinkedFromRemotePath) {
         Intent end = new Intent(getDownloadFinishMessage());
         end.putExtra(EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess());
         end.putExtra(ACCOUNT_NAME, download.getAccount().name);
         end.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath());
         end.putExtra(EXTRA_FILE_PATH, download.getSavePath());
+        if (unlinkedFromRemotePath != null) {
+            end.putExtra(EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath);
+        }
         sendStickyBroadcast(end);
     }
     
@@ -545,13 +597,15 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis
     /**
      * Sends a broadcast when a new download is added to the queue.
      * 
-     * @param download          Added download operation
+     * @param download              Added download operation
+     * @param linkedToRemotePath    Path in the downloads tree where the download was linked to
      */
-    private void sendBroadcastNewDownload(DownloadFileOperation download) {
+    private void sendBroadcastNewDownload(DownloadFileOperation download, String linkedToRemotePath) {
         Intent added = new Intent(getDownloadAddedMessage());
         added.putExtra(ACCOUNT_NAME, download.getAccount().name);
         added.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath());
         added.putExtra(EXTRA_FILE_PATH, download.getSavePath());
+        added.putExtra(EXTRA_LINKED_TO_PATH, linkedToRemotePath);
         sendStickyBroadcast(added);
     }
 

+ 5 - 5
src/com/owncloud/android/files/services/FileUploader.java

@@ -199,7 +199,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
         if (uploadType == UPLOAD_SINGLE_FILE) {
 
             if (intent.hasExtra(KEY_FILE)) {
-                files = new OCFile[] { intent.getParcelableExtra(KEY_FILE) };
+                files = new OCFile[] { (OCFile) intent.getParcelableExtra(KEY_FILE) };
 
             } else {
                 localPaths = new String[] { intent.getStringExtra(KEY_LOCAL_FILE) };
@@ -372,8 +372,8 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
          * 
          * If 'file' is a directory, returns 'true' if some of its descendant files is uploading or waiting to upload. 
          * 
-         * @param account Owncloud account where the remote file will be stored.
-         * @param file A file that could be in the queue of pending uploads
+         * @param account   ownCloud account where the remote file will be stored.
+         * @param file      A file that could be in the queue of pending uploads
          */
         public boolean isUploading(Account account, OCFile file) {
             if (account == null || file == null)
@@ -400,7 +400,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
          * 
          * @param listener      Object to notify about progress of transfer.    
          * @param account       ownCloud account holding the file of interest.
-         * @param file          {@link OCfile} of interest for listener. 
+         * @param file          {@link OCFile} of interest for listener.
          */
         public void addDatatransferProgressListener (OnDatatransferProgressListener listener, Account account, OCFile file) {
             if (account == null || file == null || listener == null) return;
@@ -415,7 +415,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
          * 
          * @param listener      Object to notify about progress of transfer.    
          * @param account       ownCloud account holding the file of interest.
-         * @param file          {@link OCfile} of interest for listener. 
+         * @param file          {@link OCFile} of interest for listener.
          */
         public void removeDatatransferProgressListener (OnDatatransferProgressListener listener, Account account, OCFile file) {
             if (account == null || file == null || listener == null) return;

+ 225 - 0
src/com/owncloud/android/files/services/IndexedForest.java

@@ -0,0 +1,225 @@
+/* ownCloud Android client application
+ *   Copyright (C) 2015 ownCloud Inc.
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License version 2,
+ *   as published by the Free Software Foundation.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.owncloud.android.files.services;
+
+import android.accounts.Account;
+import android.util.Pair;
+
+import com.owncloud.android.datamodel.OCFile;
+
+import java.io.File;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ *  Helper structure to keep the trees of folders containing any file downloading or synchronizing.
+ *
+ *  A map provides the indexation based in hashing.
+ *
+ *  A tree is created per account.
+ *
+ * @author David A. Velasco
+ */
+public class IndexedForest<V> {
+
+    private ConcurrentMap<String, Node<V>> mMap = new ConcurrentHashMap<String, Node<V>>();
+
+    private class Node<V> {
+        String mKey = null;
+        Node<V> mParent = null;
+        Set<Node<V>> mChildren = new HashSet<Node<V>>();    // TODO be careful with hash()
+        V mPayload = null;
+
+        // payload is optional
+        public Node(String key, V payload) {
+            if (key == null) {
+                throw new IllegalArgumentException("Argument key MUST NOT be null");
+            }
+            mKey = key;
+            mPayload = payload;
+        }
+
+        public Node<V> getParent() {
+            return mParent;
+        };
+
+        public Set<Node<V>> getChildren() {
+            return mChildren;
+        }
+
+        public String getKey() {
+            return mKey;
+        }
+
+        public V getPayload() {
+            return mPayload;
+        }
+
+        public void addChild(Node<V> child) {
+            mChildren.add(child);
+            child.setParent(this);
+        }
+
+        private void setParent(Node<V> parent) {
+            mParent = parent;
+        }
+
+        public boolean hasChildren() {
+            return mChildren.size() > 0;
+        }
+
+        public void removeChild(Node<V> removed) {
+            mChildren.remove(removed);
+        }
+
+        public void clearPayload() {
+            mPayload = null;
+        }
+    }
+
+
+    public /* synchronized */ Pair<String, String> putIfAbsent(Account account, String remotePath, V value) {
+        String targetKey = buildKey(account, remotePath);
+        Node<V> valuedNode = new Node(targetKey, value);
+        mMap.putIfAbsent(
+                targetKey,
+                valuedNode
+        );
+
+        String currentPath = remotePath, parentPath = null, parentKey = null;
+        Node<V> currentNode = valuedNode, parentNode = null;
+        boolean linked = false;
+        while (!OCFile.ROOT_PATH.equals(currentPath) && !linked) {
+            parentPath = new File(currentPath).getParent();
+            if (!parentPath.endsWith(OCFile.PATH_SEPARATOR)) {
+                parentPath += OCFile.PATH_SEPARATOR;
+            }
+            parentKey = buildKey(account, parentPath);
+            parentNode = mMap.get(parentKey);
+            if (parentNode == null) {
+                parentNode = new Node(parentKey, null);
+                parentNode.addChild(currentNode);
+                mMap.put(parentKey, parentNode);
+            } else {
+                parentNode.addChild(currentNode);
+                linked = true;
+            }
+            currentPath = parentPath;
+            currentNode = parentNode;
+        }
+
+        String linkedTo = OCFile.ROOT_PATH;
+        if (linked) {
+            linkedTo = parentNode.getKey().substring(account.name.length());
+        }
+        return new Pair<String, String>(targetKey, linkedTo);
+    };
+
+
+    public Pair<V, String> removePayload(Account account, String remotePath) {
+        String targetKey = buildKey(account, remotePath);
+        Node<V> target = mMap.get(targetKey);
+        if (target != null) {
+            target.clearPayload();
+            if (!target.hasChildren()) {
+                return remove(account, remotePath);
+            }
+        }
+        return new Pair<V, String>(null, null);
+    }
+
+
+    public /* synchronized */ Pair<V, String> remove(Account account, String remotePath) {
+        String targetKey = buildKey(account, remotePath);
+        Node<V> firstRemoved = mMap.remove(targetKey);
+        String unlinkedFrom = null;
+
+        if (firstRemoved != null) {
+            /// remove children
+            removeDescendants(firstRemoved);
+
+            /// remove ancestors if only here due to firstRemoved
+            Node<V> removed = firstRemoved;
+            Node<V> parent = removed.getParent();
+            boolean unlinked = false;
+            while (parent != null) {
+                parent.removeChild(removed);
+                if (!parent.hasChildren()) {
+                    removed = mMap.remove(parent.getKey());
+                    parent = removed.getParent();
+                } else {
+                    break;
+                }
+            }
+
+            if (parent != null) {
+                unlinkedFrom = parent.getKey().substring(account.name.length());
+            }
+
+            return new Pair<V, String>(firstRemoved.getPayload(), unlinkedFrom);
+        }
+
+        return new Pair<V, String>(null, null);
+    }
+
+    private void removeDescendants(Node<V> removed) {
+        Iterator<Node<V>> childrenIt = removed.getChildren().iterator();
+        Node<V> child = null;
+        while (childrenIt.hasNext()) {
+            child = childrenIt.next();
+            mMap.remove(child.getKey());
+            removeDescendants(child);
+        }
+    }
+
+    public boolean contains(Account account, String remotePath) {
+        String targetKey = buildKey(account, remotePath);
+        return mMap.containsKey(targetKey);
+    }
+
+    public /* synchronized */ V get(String key) {
+        Node<V> node = mMap.get(key);
+        if (node != null) {
+            return node.getPayload();
+        } else {
+            return null;
+        }
+    }
+
+    public V get(Account account, String remotePath) {
+        String key = buildKey(account, remotePath);
+        return get(key);
+    }
+
+
+    /**
+     * Builds a key to index files
+     *
+     * @param account       Account where the file to download is stored
+     * @param remotePath    Path of the file in the server
+     */
+    private String buildKey(Account account, String remotePath) {
+        return account.name + remotePath;
+    }
+
+
+
+}

+ 610 - 0
src/com/owncloud/android/operations/RefreshFolderOperation.java

@@ -0,0 +1,610 @@
+/* ownCloud Android client application
+ *   Copyright (C) 2012-2014 ownCloud Inc.
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License version 2,
+ *   as published by the Free Software Foundation.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.owncloud.android.operations;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+import org.apache.http.HttpStatus;
+import android.accounts.Account;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+//import android.support.v4.content.LocalBroadcastManager;
+
+import com.owncloud.android.datamodel.FileDataStorageManager;
+import com.owncloud.android.datamodel.OCFile;
+
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.resources.shares.OCShare;
+import com.owncloud.android.lib.common.operations.RemoteOperation;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.lib.resources.shares.GetRemoteSharesForFileOperation;
+import com.owncloud.android.lib.resources.files.FileUtils;
+import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation;
+import com.owncloud.android.lib.resources.files.ReadRemoteFolderOperation;
+import com.owncloud.android.lib.resources.files.RemoteFile;
+
+import com.owncloud.android.syncadapter.FileSyncAdapter;
+import com.owncloud.android.utils.FileStorageUtils;
+
+
+
+/**
+ *  Remote operation performing the synchronization of the list of files contained 
+ *  in a folder identified with its remote path.
+ *  
+ *  Fetches the list and properties of the files contained in the given folder, including their 
+ *  properties, and updates the local database with them.
+ *  
+ *  Does NOT enter in the child folders to synchronize their contents also.
+ * 
+ *  @author David A. Velasco
+ */
+public class RefreshFolderOperation extends RemoteOperation {
+
+    private static final String TAG = RefreshFolderOperation.class.getSimpleName();
+
+    public static final String EVENT_SINGLE_FOLDER_CONTENTS_SYNCED  = 
+            RefreshFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_CONTENTS_SYNCED";
+    public static final String EVENT_SINGLE_FOLDER_SHARES_SYNCED    = 
+            RefreshFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_SHARES_SYNCED";
+    
+    /** Time stamp for the synchronization process in progress */
+    private long mCurrentSyncTime;
+    
+    /** Remote folder to synchronize */
+    private OCFile mLocalFolder;
+    
+    /** Access to the local database */
+    private FileDataStorageManager mStorageManager;
+    
+    /** Account where the file to synchronize belongs */
+    private Account mAccount;
+    
+    /** Android context; necessary to send requests to the download service */
+    private Context mContext;
+    
+    /** Files and folders contained in the synchronized folder after a successful operation */
+    private List<OCFile> mChildren;
+
+    /** Counter of conflicts found between local and remote files */
+    private int mConflictsFound;
+
+    /** Counter of failed operations in synchronization of kept-in-sync files */
+    private int mFailsInFavouritesFound;
+
+    /**
+     * Map of remote and local paths to files that where locally stored in a location 
+     * out of the ownCloud folder and couldn't be copied automatically into it 
+     **/
+    private Map<String, String> mForgottenLocalFiles;
+
+    /** 'True' means that this operation is part of a full account synchronization */ 
+    private boolean mSyncFullAccount;
+
+    /** 'True' means that Share resources bound to the files into should be refreshed also */
+    private boolean mIsShareSupported;
+    
+    /** 'True' means that the remote folder changed and should be fetched */
+    private boolean mRemoteFolderChanged;
+
+    /** 'True' means that Etag will be ignored */
+    private boolean mIgnoreETag;
+
+    
+    /**
+     * Creates a new instance of {@link RefreshFolderOperation}.
+     * 
+     * @param   folder                  Folder to synchronize.
+     * @param   currentSyncTime         Time stamp for the synchronization process in progress.
+     * @param   syncFullAccount         'True' means that this operation is part of a full account 
+     *                                  synchronization.
+     * @param   isShareSupported        'True' means that the server supports the sharing API.           
+     * @param   ignoreEtag              'True' means that the content of the remote folder should
+     *                                  be fetched and updated even though the 'eTag' did not 
+     *                                  change.  
+     * @param   dataStorageManager      Interface with the local database.
+     * @param   account                 ownCloud account where the folder is located. 
+     * @param   context                 Application context.
+     */
+    public RefreshFolderOperation(OCFile folder,
+                                  long currentSyncTime,
+                                  boolean syncFullAccount,
+                                  boolean isShareSupported,
+                                  boolean ignoreETag,
+                                  FileDataStorageManager dataStorageManager,
+                                  Account account,
+                                  Context context) {
+        mLocalFolder = folder;
+        mCurrentSyncTime = currentSyncTime;
+        mSyncFullAccount = syncFullAccount;
+        mIsShareSupported = isShareSupported;
+        mStorageManager = dataStorageManager;
+        mAccount = account;
+        mContext = context;
+        mForgottenLocalFiles = new HashMap<String, String>();
+        mRemoteFolderChanged = false;
+        mIgnoreETag = ignoreETag;
+    }
+    
+    
+    public int getConflictsFound() {
+        return mConflictsFound;
+    }
+    
+    public int getFailsInFavouritesFound() {
+        return mFailsInFavouritesFound;
+    }
+    
+    public Map<String, String> getForgottenLocalFiles() {
+        return mForgottenLocalFiles;
+    }
+    
+    /**
+     * Returns the list of files and folders contained in the synchronized folder, 
+     * if called after synchronization is complete.
+     * 
+     * @return  List of files and folders contained in the synchronized folder.
+     */
+    public List<OCFile> getChildren() {
+        return mChildren;
+    }
+    
+    /**
+     * Performs the synchronization.
+     * 
+     * {@inheritDoc}
+     */
+    @Override
+    protected RemoteOperationResult run(OwnCloudClient client) {
+        RemoteOperationResult result = null;
+        mFailsInFavouritesFound = 0;
+        mConflictsFound = 0;
+        mForgottenLocalFiles.clear();
+        
+        if (FileUtils.PATH_SEPARATOR.equals(mLocalFolder.getRemotePath()) && !mSyncFullAccount) {
+            updateOCVersion(client);
+        }
+        
+        result = checkForChanges(client);
+        
+        if (result.isSuccess()) {
+            if (mRemoteFolderChanged) {
+                result = fetchAndSyncRemoteFolder(client);
+            } else {
+                mChildren = mStorageManager.getFolderContent(mLocalFolder);
+            }
+        }
+        
+        if (!mSyncFullAccount) {            
+            sendLocalBroadcast(
+                    EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result
+            );
+        }
+        
+        if (result.isSuccess() && mIsShareSupported && !mSyncFullAccount) {
+            refreshSharesForFolder(client); // share result is ignored 
+        }
+        
+        if (!mSyncFullAccount) {            
+            sendLocalBroadcast(
+                    EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result
+            );
+        }
+        
+        return result;
+        
+    }
+
+
+    private void updateOCVersion(OwnCloudClient client) {
+        UpdateOCVersionOperation update = new UpdateOCVersionOperation(mAccount, mContext);
+        RemoteOperationResult result = update.execute(client);
+        if (result.isSuccess()) {
+            mIsShareSupported = update.getOCVersion().isSharedSupported();
+        }
+    }
+
+    
+    private RemoteOperationResult checkForChanges(OwnCloudClient client) {
+        mRemoteFolderChanged = true;
+        RemoteOperationResult result = null;
+        String remotePath = null;
+
+        remotePath = mLocalFolder.getRemotePath();
+        Log_OC.d(TAG, "Checking changes in " + mAccount.name + remotePath);
+        
+        // remote request 
+        ReadRemoteFileOperation operation = new ReadRemoteFileOperation(remotePath);
+        result = operation.execute(client);
+        if (result.isSuccess()){
+            OCFile remoteFolder = FileStorageUtils.fillOCFile((RemoteFile) result.getData().get(0));
+
+            if (!mIgnoreETag) {
+                // check if remote and local folder are different
+                mRemoteFolderChanged = 
+                        !(remoteFolder.getEtag().equalsIgnoreCase(mLocalFolder.getEtag()));
+            }
+
+            result = new RemoteOperationResult(ResultCode.OK);
+        
+            Log_OC.i(TAG, "Checked " + mAccount.name + remotePath + " : " + 
+                    (mRemoteFolderChanged ? "changed" : "not changed"));
+            
+        } else {
+            // check failed
+            if (result.getCode() == ResultCode.FILE_NOT_FOUND) {
+                removeLocalFolder();
+            }
+            if (result.isException()) {
+                Log_OC.e(TAG, "Checked " + mAccount.name + remotePath  + " : " + 
+                        result.getLogMessage(), result.getException());
+            } else {
+                Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + 
+                        result.getLogMessage());
+            }
+        }
+        
+        return result;
+    }
+
+
+    private RemoteOperationResult fetchAndSyncRemoteFolder(OwnCloudClient client) {
+        String remotePath = mLocalFolder.getRemotePath();
+        ReadRemoteFolderOperation operation = new ReadRemoteFolderOperation(remotePath);
+        RemoteOperationResult result = operation.execute(client);
+        Log_OC.d(TAG, "Synchronizing " + mAccount.name + remotePath);
+        
+        if (result.isSuccess()) {
+            synchronizeData(result.getData(), client);
+            if (mConflictsFound > 0  || mFailsInFavouritesFound > 0) { 
+                result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);   
+                    // should be a different result code, but will do the job
+            }
+        } else {
+            if (result.getCode() == ResultCode.FILE_NOT_FOUND)
+                removeLocalFolder();
+        }
+        
+        return result;
+    }
+
+    
+    private void removeLocalFolder() {
+        if (mStorageManager.fileExists(mLocalFolder.getFileId())) {
+            String currentSavePath = FileStorageUtils.getSavePath(mAccount.name);
+            mStorageManager.removeFolder(
+                    mLocalFolder, 
+                    true, 
+                    (   mLocalFolder.isDown() && 
+                            mLocalFolder.getStoragePath().startsWith(currentSavePath)
+                    )
+            );
+        }
+    }
+
+
+    /**
+     *  Synchronizes the data retrieved from the server about the contents of the target folder 
+     *  with the current data in the local database.
+     *  
+     *  Grants that mChildren is updated with fresh data after execution.
+     *  
+     *  @param folderAndFiles   Remote folder and children files in Folder 
+     *  
+     *  @param client           Client instance to the remote server where the data were 
+     *                          retrieved.  
+     *  @return                 'True' when any change was made in the local data, 'false' otherwise
+     */
+    private void synchronizeData(ArrayList<Object> folderAndFiles, OwnCloudClient client) {
+        // get 'fresh data' from the database
+        mLocalFolder = mStorageManager.getFileByPath(mLocalFolder.getRemotePath());
+
+        // parse data from remote folder 
+        OCFile remoteFolder = fillOCFile((RemoteFile)folderAndFiles.get(0));
+        remoteFolder.setParentId(mLocalFolder.getParentId());
+        remoteFolder.setFileId(mLocalFolder.getFileId());
+        
+        Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath() 
+                + " changed - starting update of local data ");
+        
+        List<OCFile> updatedFiles = new Vector<OCFile>(folderAndFiles.size() - 1);
+        List<SynchronizeFileOperation> filesToSyncContents = new Vector<SynchronizeFileOperation>();
+
+        // get current data about local contents of the folder to synchronize
+        List<OCFile> localFiles = mStorageManager.getFolderContent(mLocalFolder);
+        Map<String, OCFile> localFilesMap = new HashMap<String, OCFile>(localFiles.size());
+        for (OCFile file : localFiles) {
+            localFilesMap.put(file.getRemotePath(), file);
+        }
+        
+        // loop to update every child
+        OCFile remoteFile = null, localFile = null;
+        for (int i=1; i<folderAndFiles.size(); i++) {
+            /// new OCFile instance with the data from the server
+            remoteFile = fillOCFile((RemoteFile)folderAndFiles.get(i));
+            remoteFile.setParentId(mLocalFolder.getFileId());
+
+            /// retrieve local data for the read file 
+            //  localFile = mStorageManager.getFileByPath(remoteFile.getRemotePath());
+            localFile = localFilesMap.remove(remoteFile.getRemotePath());
+            
+            /// add to the remoteFile (the new one) data about LOCAL STATE (not existing in server)
+            remoteFile.setLastSyncDateForProperties(mCurrentSyncTime);
+            if (localFile != null) {
+                // some properties of local state are kept unmodified
+                remoteFile.setFileId(localFile.getFileId());
+                remoteFile.setKeepInSync(localFile.keepInSync());
+                remoteFile.setLastSyncDateForData(localFile.getLastSyncDateForData());
+                remoteFile.setModificationTimestampAtLastSyncForData(
+                        localFile.getModificationTimestampAtLastSyncForData()
+                );
+                remoteFile.setStoragePath(localFile.getStoragePath());
+                // eTag will not be updated unless contents are synchronized 
+                //  (Synchronize[File|Folder]Operation with remoteFile as parameter)
+                remoteFile.setEtag(localFile.getEtag());    
+                if (remoteFile.isFolder()) {
+                    remoteFile.setFileLength(localFile.getFileLength()); 
+                        // TODO move operations about size of folders to FileContentProvider
+                } else if (mRemoteFolderChanged && remoteFile.isImage() &&
+                        remoteFile.getModificationTimestamp() != localFile.getModificationTimestamp()) {
+                    remoteFile.setNeedsUpdateThumbnail(true);
+                    Log.d(TAG, "Image " + remoteFile.getFileName() + " updated on the server");
+                }
+                remoteFile.setPublicLink(localFile.getPublicLink());
+                remoteFile.setShareByLink(localFile.isShareByLink());
+            } else {
+                // remote eTag will not be updated unless contents are synchronized 
+                //  (Synchronize[File|Folder]Operation with remoteFile as parameter)
+                remoteFile.setEtag(""); 
+            }
+
+            /// check and fix, if needed, local storage path
+            checkAndFixForeignStoragePath(remoteFile);      // policy - local files are COPIED 
+                                                            // into the ownCloud local folder;
+            searchForLocalFileInDefaultPath(remoteFile);    // legacy   
+
+            /// prepare content synchronization for kept-in-sync files
+            if (remoteFile.keepInSync()) {
+                SynchronizeFileOperation operation = new SynchronizeFileOperation(  localFile,        
+                                                                                    remoteFile, 
+                                                                                    mAccount, 
+                                                                                    true, 
+                                                                                    mContext
+                                                                                    );
+                
+                filesToSyncContents.add(operation);
+            }
+            
+            updatedFiles.add(remoteFile);
+        }
+
+        // save updated contents in local database
+        mStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
+
+        // request for the synchronization of file contents AFTER saving current remote properties
+        startContentSynchronizations(filesToSyncContents, client);
+
+        mChildren = updatedFiles;
+    }
+
+    /**
+     * Performs a list of synchronization operations, determining if a download or upload is needed
+     * or if exists conflict due to changes both in local and remote contents of the each file.
+     * 
+     * If download or upload is needed, request the operation to the corresponding service and goes 
+     * on.
+     * 
+     * @param filesToSyncContents       Synchronization operations to execute.
+     * @param client                    Interface to the remote ownCloud server.
+     */
+    private void startContentSynchronizations(
+            List<SynchronizeFileOperation> filesToSyncContents, OwnCloudClient client
+        ) {
+        RemoteOperationResult contentsResult = null;
+        for (SynchronizeFileOperation op: filesToSyncContents) {
+            contentsResult = op.execute(mStorageManager, mContext);   // async
+            if (!contentsResult.isSuccess()) {
+                if (contentsResult.getCode() == ResultCode.SYNC_CONFLICT) {
+                    mConflictsFound++;
+                } else {
+                    mFailsInFavouritesFound++;
+                    if (contentsResult.getException() != null) {
+                        Log_OC.e(TAG, "Error while synchronizing favourites : " 
+                                +  contentsResult.getLogMessage(), contentsResult.getException());
+                    } else {
+                        Log_OC.e(TAG, "Error while synchronizing favourites : " 
+                                + contentsResult.getLogMessage());
+                    }
+                }
+            }   // won't let these fails break the synchronization process
+        }
+    }
+
+
+    public boolean isMultiStatus(int status) {
+        return (status == HttpStatus.SC_MULTI_STATUS); 
+    }
+
+    /**
+     * Creates and populates a new {@link OCFile} object with the data read from the server.
+     * 
+     * @param remote    remote file read from the server (remote file or folder).
+     * @return          New OCFile instance representing the remote resource described by we.
+     */
+    private OCFile fillOCFile(RemoteFile remote) {
+        OCFile file = new OCFile(remote.getRemotePath());
+        file.setCreationTimestamp(remote.getCreationTimestamp());
+        file.setFileLength(remote.getLength());
+        file.setMimetype(remote.getMimeType());
+        file.setModificationTimestamp(remote.getModifiedTimestamp());
+        file.setEtag(remote.getEtag());
+        file.setPermissions(remote.getPermissions());
+        file.setRemoteId(remote.getRemoteId());
+        return file;
+    }
+    
+
+    /**
+     * Checks the storage path of the OCFile received as parameter. 
+     * If it's out of the local ownCloud folder, tries to copy the file inside it. 
+     * 
+     * If the copy fails, the link to the local file is nullified. The account of forgotten 
+     * files is kept in {@link #mForgottenLocalFiles}
+     *) 
+     * @param file      File to check and fix.
+     */
+    private void checkAndFixForeignStoragePath(OCFile file) {
+        String storagePath = file.getStoragePath();
+        String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, file);
+        if (storagePath != null && !storagePath.equals(expectedPath)) {
+            /// fix storagePaths out of the local ownCloud folder
+            File originalFile = new File(storagePath);
+            if (FileStorageUtils.getUsableSpace(mAccount.name) < originalFile.length()) {
+                mForgottenLocalFiles.put(file.getRemotePath(), storagePath);
+                file.setStoragePath(null);
+                    
+            } else {
+                InputStream in = null;
+                OutputStream out = null;
+                try {
+                    File expectedFile = new File(expectedPath);
+                    File expectedParent = expectedFile.getParentFile();
+                    expectedParent.mkdirs();
+                    if (!expectedParent.isDirectory()) {
+                        throw new IOException(
+                                "Unexpected error: parent directory could not be created"
+                        );
+                    }
+                    expectedFile.createNewFile();
+                    if (!expectedFile.isFile()) {
+                        throw new IOException("Unexpected error: target file could not be created");
+                    }                    
+                    in = new FileInputStream(originalFile);
+                    out = new FileOutputStream(expectedFile);
+                    byte[] buf = new byte[1024];
+                    int len;
+                    while ((len = in.read(buf)) > 0){
+                        out.write(buf, 0, len);
+                    }
+                    file.setStoragePath(expectedPath);
+                    
+                } catch (Exception e) {
+                    Log_OC.e(TAG, "Exception while copying foreign file " + expectedPath, e);
+                    mForgottenLocalFiles.put(file.getRemotePath(), storagePath);
+                    file.setStoragePath(null);
+                    
+                } finally {
+                    try {
+                        if (in != null) in.close();
+                    } catch (Exception e) {
+                        Log_OC.d(TAG, "Weird exception while closing input stream for " 
+                                + storagePath + " (ignoring)", e);
+                    }
+                    try {
+                        if (out != null) out.close();
+                    } catch (Exception e) {
+                        Log_OC.d(TAG, "Weird exception while closing output stream for " 
+                                + expectedPath + " (ignoring)", e);
+                    }
+                }
+            }
+        }
+    }
+    
+    
+    private RemoteOperationResult refreshSharesForFolder(OwnCloudClient client) {
+        RemoteOperationResult result = null;
+        
+        // remote request 
+        GetRemoteSharesForFileOperation operation = 
+                new GetRemoteSharesForFileOperation(mLocalFolder.getRemotePath(), false, true);
+        result = operation.execute(client);
+        
+        if (result.isSuccess()) {
+            // update local database
+            ArrayList<OCShare> shares = new ArrayList<OCShare>();
+            for(Object obj: result.getData()) {
+                shares.add((OCShare) obj);
+            }
+            mStorageManager.saveSharesInFolder(shares, mLocalFolder);
+        }
+
+        return result;
+    }
+    
+
+    /**
+     * Scans the default location for saving local copies of files searching for
+     * a 'lost' file with the same full name as the {@link OCFile} received as 
+     * parameter.
+     *  
+     * @param file      File to associate a possible 'lost' local file.
+     */
+    private void searchForLocalFileInDefaultPath(OCFile file) {
+        if (file.getStoragePath() == null && !file.isFolder()) {
+            File f = new File(FileStorageUtils.getDefaultSavePathFor(mAccount.name, file));
+            if (f.exists()) {
+                file.setStoragePath(f.getAbsolutePath());
+                file.setLastSyncDateForData(f.lastModified());
+            }
+        }
+    }
+
+    
+    /**
+     * Sends a message to any application component interested in the progress 
+     * of the synchronization.
+     * 
+     * @param event
+     * @param dirRemotePath     Remote path of a folder that was just synchronized 
+     *                          (with or without success)
+     * @param result
+     */
+    private void sendLocalBroadcast(
+            String event, String dirRemotePath, RemoteOperationResult result
+        ) {
+        Log_OC.d(TAG, "Send broadcast " + event);
+        Intent intent = new Intent(event);
+        intent.putExtra(FileSyncAdapter.EXTRA_ACCOUNT_NAME, mAccount.name);
+        if (dirRemotePath != null) {
+            intent.putExtra(FileSyncAdapter.EXTRA_FOLDER_PATH, dirRemotePath);
+        }
+        intent.putExtra(FileSyncAdapter.EXTRA_RESULT, result);
+        mContext.sendStickyBroadcast(intent);
+        //LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
+    }
+
+
+    public boolean getRemoteFolderChanged() {
+        return mRemoteFolderChanged;
+    }
+
+}

+ 3 - 4
src/com/owncloud/android/operations/RenameFileOperation.java

@@ -51,7 +51,6 @@ public class RenameFileOperation extends SyncOperation {
      * Constructor
      * 
      * @param remotePath            RemotePath of the OCFile instance describing the remote file or folder to rename
-     * @param account               OwnCloud account containing the remote file 
      * @param newName               New name to set as the name of file.
      */
     public RenameFileOperation(String remotePath, String newName) {
@@ -117,7 +116,7 @@ public class RenameFileOperation extends SyncOperation {
 
     private void saveLocalFile() {
         mFile.setFileName(mNewName);
-        
+
         // try to rename the local copy of the file
         if (mFile.isDown()) {
             String oldPath = mFile.getStoragePath();
@@ -129,8 +128,8 @@ public class RenameFileOperation extends SyncOperation {
                 String newPath = parentStoragePath + mNewName;
                 mFile.setStoragePath(newPath);
 
-                // notify MediaScanner about removed file - TODO really works?
-                getStorageManager().triggerMediaScan(oldPath);
+                // notify MediaScanner about removed file
+                getStorageManager().deleteFileInMediaScan(oldPath);
                 // notify to scan about new file
                 getStorageManager().triggerMediaScan(newPath);
             }

+ 74 - 14
src/com/owncloud/android/operations/SynchronizeFileOperation.java

@@ -54,13 +54,22 @@ public class SynchronizeFileOperation extends SyncOperation {
     
     private boolean mTransferWasRequested = false;
 
+    /** 
+     * When 'false', uploads to the server are not done; only downloads or conflict detection.  
+     * This is a temporal field. 
+     * TODO Remove when 'folder synchronization' replaces 'folder download'.
+     */    
+    private boolean mAllowUploads;
+
     
     /**
-     * Constructor.
+     * Constructor for "full synchronization mode".
      * 
-     * Uses remotePath to retrieve all the data in local cache and remote server when the operation
+     * Uses remotePath to retrieve all the data both in local cache and in the remote OC server when the operation
      * is executed, instead of reusing {@link OCFile} instances.
      * 
+     * Useful for direct synchronization of a single file.
+     * 
      * @param 
      * @param account               ownCloud account holding the file.
      * @param syncFileContents      When 'true', transference of data will be started by the 
@@ -79,16 +88,21 @@ public class SynchronizeFileOperation extends SyncOperation {
         mAccount = account;
         mSyncFileContents = syncFileContents;
         mContext = context;
+        mAllowUploads = true;
     }
 
     
     /**
-     * Constructor allowing to reuse {@link OCFile} instances just queried from cache or network.
+     * Constructor allowing to reuse {@link OCFile} instances just queried from local cache or from remote OC server.
      * 
-     * Useful for folder / account synchronizations.
+     * Useful to include this operation as part of the synchronization of a folder (or a full account), avoiding the
+     * repetition of fetch operations (both in local database or remote server).
      * 
-     * @param localFile             Data of file currently hold in device cache. MUSTN't be null.
-     * @param serverFile            Data of file just retrieved from network. If null, will be
+     * At least one of localFile or serverFile MUST NOT BE NULL. If you don't have none of them, use the other 
+     * constructor.
+     * 
+     * @param localFile             Data of file (just) retrieved from local cache/database.
+     * @param serverFile            Data of file (just) retrieved from a remote server. If null, will be
      *                              retrieved from network by the operation when executed.
      * @param account               ownCloud account holding the file.
      * @param syncFileContents      When 'true', transference of data will be started by the 
@@ -104,10 +118,53 @@ public class SynchronizeFileOperation extends SyncOperation {
         
         mLocalFile = localFile;
         mServerFile = serverFile;
-        mRemotePath = localFile.getRemotePath();
+        if (mLocalFile != null) {
+            mRemotePath = mLocalFile.getRemotePath();
+            if (mServerFile != null && !mServerFile.getRemotePath().equals(mRemotePath)) {
+                throw new IllegalArgumentException("serverFile and localFile do not correspond to the same OC file");
+            }
+        } else if (mServerFile != null) {
+            mRemotePath = mServerFile.getRemotePath();
+        } else {
+            throw new IllegalArgumentException("Both serverFile and localFile are NULL");
+        }
         mAccount = account;
         mSyncFileContents = syncFileContents;
         mContext = context;
+        mAllowUploads = true;
+    }
+    
+
+    /**
+     * Temporal constructor.
+     * 
+     * Extends the previous one to allow constrained synchronizations where uploads are never performed - only
+     * downloads or conflict detection.
+     * 
+     * Do not use unless you are involved in 'folder synchronization' or 'folder download' work in progress.
+     * 
+     * TODO Remove when 'folder synchronization' replaces 'folder download'.
+     * 
+     * @param localFile             Data of file (just) retrieved from local cache/database. MUSTN't be null.
+     * @param serverFile            Data of file (just) retrieved from a remote server. If null, will be
+     *                              retrieved from network by the operation when executed.
+     * @param account               ownCloud account holding the file.
+     * @param syncFileContents      When 'true', transference of data will be started by the 
+     *                              operation if needed and no conflict is detected.
+     * @param allowUploads          When 'false', uploads to the server are not done; only downloads or conflict
+     *                              detection. 
+     * @param context               Android context; needed to start transfers.
+     */
+    public SynchronizeFileOperation(
+            OCFile localFile,
+            OCFile serverFile, 
+            Account account, 
+            boolean syncFileContents,
+            boolean allowUploads, 
+            Context context) {
+        
+        this(localFile, serverFile, account, syncFileContents, context);
+        mAllowUploads = allowUploads;
     }
     
 
@@ -145,13 +202,15 @@ public class SynchronizeFileOperation extends SyncOperation {
                 boolean serverChanged = false;
                 /* time for eTag is coming, but not yet
                     if (mServerFile.getEtag() != null) {
-                        serverChanged = (!mServerFile.getEtag().equals(mLocalFile.getEtag()));   // TODO could this be dangerous when the user upgrades the server from non-tagged to tagged?
+                        serverChanged = (!mServerFile.getEtag().equals(mLocalFile.getEtag()));
                     } else { */
-                // server without etags
-                serverChanged = (mServerFile.getModificationTimestamp() != mLocalFile.getModificationTimestampAtLastSyncForData());
+                serverChanged = (
+                    mServerFile.getModificationTimestamp() != mLocalFile.getModificationTimestampAtLastSyncForData()
+                );
                 //}
-                boolean localChanged = (mLocalFile.getLocalModificationTimestamp() > mLocalFile.getLastSyncDateForData());
-                // TODO this will be always true after the app is upgraded to database version 2; will result in unnecessary uploads
+                boolean localChanged = (
+                    mLocalFile.getLocalModificationTimestamp() > mLocalFile.getLastSyncDateForData()
+                );
 
                 /// decide action to perform depending upon changes
                 //if (!mLocalFile.getEtag().isEmpty() && localChanged && serverChanged) {
@@ -159,7 +218,7 @@ public class SynchronizeFileOperation extends SyncOperation {
                     result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);
 
                 } else if (localChanged) {
-                    if (mSyncFileContents) {
+                    if (mSyncFileContents && mAllowUploads) {
                         requestForUpload(mLocalFile);
                         // the local update of file properties will be done by the FileUploader service when the upload finishes
                     } else {
@@ -195,7 +254,8 @@ public class SynchronizeFileOperation extends SyncOperation {
 
         }
 
-        Log_OC.i(TAG, "Synchronizing " + mAccount.name + ", file " + mLocalFile.getRemotePath() + ": " + result.getLogMessage());
+        Log_OC.i(TAG, "Synchronizing " + mAccount.name + ", file " + mLocalFile.getRemotePath() + ": " 
+                + result.getLogMessage());
 
         return result;
     }

+ 263 - 341
src/com/owncloud/android/operations/SynchronizeFolderOperation.java

@@ -17,43 +17,35 @@
 
 package com.owncloud.android.operations;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Vector;
-
-import org.apache.http.HttpStatus;
 import android.accounts.Account;
 import android.content.Context;
 import android.content.Intent;
 import android.util.Log;
-//import android.support.v4.content.LocalBroadcastManager;
 
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
-
+import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.lib.common.OwnCloudClient;
-import com.owncloud.android.lib.resources.shares.OCShare;
-import com.owncloud.android.lib.common.operations.RemoteOperation;
+import com.owncloud.android.lib.common.operations.OperationCancelledException;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
 import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.lib.resources.shares.GetRemoteSharesForFileOperation;
-import com.owncloud.android.lib.resources.files.FileUtils;
 import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation;
 import com.owncloud.android.lib.resources.files.ReadRemoteFolderOperation;
 import com.owncloud.android.lib.resources.files.RemoteFile;
-
-import com.owncloud.android.syncadapter.FileSyncAdapter;
+import com.owncloud.android.operations.common.SyncOperation;
+import com.owncloud.android.services.OperationsService;
 import com.owncloud.android.utils.FileStorageUtils;
 
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+//import android.support.v4.content.LocalBroadcastManager;
 
 
 /**
@@ -67,225 +59,177 @@ import com.owncloud.android.utils.FileStorageUtils;
  * 
  *  @author David A. Velasco
  */
-public class SynchronizeFolderOperation extends RemoteOperation {
+public class SynchronizeFolderOperation extends SyncOperation {
 
     private static final String TAG = SynchronizeFolderOperation.class.getSimpleName();
 
-    public static final String EVENT_SINGLE_FOLDER_CONTENTS_SYNCED  = 
-            SynchronizeFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_CONTENTS_SYNCED";
-    public static final String EVENT_SINGLE_FOLDER_SHARES_SYNCED    = 
-            SynchronizeFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_SHARES_SYNCED";
-    
     /** Time stamp for the synchronization process in progress */
     private long mCurrentSyncTime;
-    
-    /** Remote folder to synchronize */
-    private OCFile mLocalFolder;
-    
-    /** Access to the local database */
-    private FileDataStorageManager mStorageManager;
+
+    /** Remote path of the folder to synchronize */
+    private String mRemotePath;
     
     /** Account where the file to synchronize belongs */
     private Account mAccount;
-    
+
     /** Android context; necessary to send requests to the download service */
     private Context mContext;
-    
+
+    /** Locally cached information about folder to synchronize */
+    private OCFile mLocalFolder;
+
     /** Files and folders contained in the synchronized folder after a successful operation */
-    private List<OCFile> mChildren;
+    //private List<OCFile> mChildren;
 
     /** Counter of conflicts found between local and remote files */
     private int mConflictsFound;
 
     /** Counter of failed operations in synchronization of kept-in-sync files */
-    private int mFailsInFavouritesFound;
+    private int mFailsInFileSyncsFound;
 
-    /**
-     * Map of remote and local paths to files that where locally stored in a location 
-     * out of the ownCloud folder and couldn't be copied automatically into it 
-     **/
-    private Map<String, String> mForgottenLocalFiles;
-
-    /** 'True' means that this operation is part of a full account synchronization */ 
-    private boolean mSyncFullAccount;
-
-    /** 'True' means that Share resources bound to the files into should be refreshed also */
-    private boolean mIsShareSupported;
-    
     /** 'True' means that the remote folder changed and should be fetched */
     private boolean mRemoteFolderChanged;
 
-    /** 'True' means that Etag will be ignored */
-    private boolean mIgnoreETag;
-
+    private List<OCFile> mFilesForDirectDownload;
+        // to avoid extra PROPFINDs when there was no change in the folder
     
+    private List<SyncOperation> mFilesToSyncContentsWithoutUpload;
+        // this will go out when 'folder synchronization' replaces 'folder download'; step by step  
+
+    private List<SyncOperation> mFavouriteFilesToSyncContents;
+        // this will be used for every file when 'folder synchronization' replaces 'folder download' 
+
+    private final AtomicBoolean mCancellationRequested;
+
     /**
      * Creates a new instance of {@link SynchronizeFolderOperation}.
-     * 
-     * @param   folder                  Folder to synchronize.
-     * @param   currentSyncTime         Time stamp for the synchronization process in progress.
-     * @param   syncFullAccount         'True' means that this operation is part of a full account 
-     *                                  synchronization.
-     * @param   isShareSupported        'True' means that the server supports the sharing API.           
-     * @param   ignoreEtag              'True' means that the content of the remote folder should
-     *                                  be fetched and updated even though the 'eTag' did not 
-     *                                  change.  
-     * @param   dataStorageManager      Interface with the local database.
-     * @param   account                 ownCloud account where the folder is located. 
+     *
      * @param   context                 Application context.
+     * @param   remotePath              Path to synchronize.
+     * @param   account                 ownCloud account where the folder is located.
+     * @param   currentSyncTime         Time stamp for the synchronization process in progress.
      */
-    public SynchronizeFolderOperation(  OCFile folder, 
-                                        long currentSyncTime, 
-                                        boolean syncFullAccount,
-                                        boolean isShareSupported,
-                                        boolean ignoreETag,
-                                        FileDataStorageManager dataStorageManager, 
-                                        Account account, 
-                                        Context context ) {
-        mLocalFolder = folder;
+    public SynchronizeFolderOperation(Context context, String remotePath, Account account, long currentSyncTime){
+        mRemotePath = remotePath;
         mCurrentSyncTime = currentSyncTime;
-        mSyncFullAccount = syncFullAccount;
-        mIsShareSupported = isShareSupported;
-        mStorageManager = dataStorageManager;
         mAccount = account;
         mContext = context;
-        mForgottenLocalFiles = new HashMap<String, String>();
         mRemoteFolderChanged = false;
-        mIgnoreETag = ignoreETag;
+        mFilesForDirectDownload = new Vector<OCFile>();
+        mFilesToSyncContentsWithoutUpload = new Vector<SyncOperation>();
+        mFavouriteFilesToSyncContents = new Vector<SyncOperation>();
+        mCancellationRequested = new AtomicBoolean(false);
     }
-    
-    
+
+
     public int getConflictsFound() {
         return mConflictsFound;
     }
-    
-    public int getFailsInFavouritesFound() {
-        return mFailsInFavouritesFound;
-    }
-    
-    public Map<String, String> getForgottenLocalFiles() {
-        return mForgottenLocalFiles;
-    }
-    
-    /**
-     * Returns the list of files and folders contained in the synchronized folder, 
-     * if called after synchronization is complete.
-     * 
-     * @return  List of files and folders contained in the synchronized folder.
-     */
-    public List<OCFile> getChildren() {
-        return mChildren;
+
+    public int getFailsInFileSyncsFound() {
+        return mFailsInFileSyncsFound;
     }
-    
+
     /**
      * Performs the synchronization.
-     * 
+     *
      * {@inheritDoc}
      */
     @Override
     protected RemoteOperationResult run(OwnCloudClient client) {
         RemoteOperationResult result = null;
-        mFailsInFavouritesFound = 0;
+        mFailsInFileSyncsFound = 0;
         mConflictsFound = 0;
-        mForgottenLocalFiles.clear();
-        
-        if (FileUtils.PATH_SEPARATOR.equals(mLocalFolder.getRemotePath()) && !mSyncFullAccount) {
-            updateOCVersion(client);
-        }
         
-        result = checkForChanges(client);
-        
-        if (result.isSuccess()) {
-            if (mRemoteFolderChanged) {
-                result = fetchAndSyncRemoteFolder(client);
-            } else {
-                mChildren = mStorageManager.getFolderContent(mLocalFolder);
+        try {
+            // get locally cached information about folder 
+            mLocalFolder = getStorageManager().getFileByPath(mRemotePath);   
+            
+            result = checkForChanges(client);
+    
+            if (result.isSuccess()) {
+                if (mRemoteFolderChanged) {
+                    result = fetchAndSyncRemoteFolder(client);
+                    
+                } else {
+                    prepareOpsFromLocalKnowledge();
+                }
+                
+                if (result.isSuccess()) {
+                    syncContents(client);
+                }
+
             }
+            
+            if (mCancellationRequested.get()) {
+                throw new OperationCancelledException();
+            }
+            
+        } catch (OperationCancelledException e) {
+            result = new RemoteOperationResult(e);
         }
-        
-        if (!mSyncFullAccount) {            
-            sendLocalBroadcast(
-                    EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result
-            );
-        }
-        
-        if (result.isSuccess() && mIsShareSupported && !mSyncFullAccount) {
-            refreshSharesForFolder(client); // share result is ignored 
-        }
-        
-        if (!mSyncFullAccount) {            
-            sendLocalBroadcast(
-                    EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result
-            );
-        }
-        
-        return result;
-        
-    }
 
+        return result;
 
-    private void updateOCVersion(OwnCloudClient client) {
-        UpdateOCVersionOperation update = new UpdateOCVersionOperation(mAccount, mContext);
-        RemoteOperationResult result = update.execute(client);
-        if (result.isSuccess()) {
-            mIsShareSupported = update.getOCVersion().isSharedSupported();
-        }
     }
 
-    
-    private RemoteOperationResult checkForChanges(OwnCloudClient client) {
+    private RemoteOperationResult checkForChanges(OwnCloudClient client) throws OperationCancelledException {
+        Log_OC.d(TAG, "Checking changes in " + mAccount.name + mRemotePath);
+
         mRemoteFolderChanged = true;
         RemoteOperationResult result = null;
-        String remotePath = null;
-
-        remotePath = mLocalFolder.getRemotePath();
-        Log_OC.d(TAG, "Checking changes in " + mAccount.name + remotePath);
         
-        // remote request 
-        ReadRemoteFileOperation operation = new ReadRemoteFileOperation(remotePath);
+        if (mCancellationRequested.get()) {
+            throw new OperationCancelledException();
+        }
+        
+        // remote request
+        ReadRemoteFileOperation operation = new ReadRemoteFileOperation(mRemotePath);
         result = operation.execute(client);
         if (result.isSuccess()){
             OCFile remoteFolder = FileStorageUtils.fillOCFile((RemoteFile) result.getData().get(0));
 
-            if (!mIgnoreETag) {
-                // check if remote and local folder are different
-                mRemoteFolderChanged = 
+            // check if remote and local folder are different
+            mRemoteFolderChanged =
                         !(remoteFolder.getEtag().equalsIgnoreCase(mLocalFolder.getEtag()));
-            }
 
             result = new RemoteOperationResult(ResultCode.OK);
-        
-            Log_OC.i(TAG, "Checked " + mAccount.name + remotePath + " : " + 
+
+            Log_OC.i(TAG, "Checked " + mAccount.name + mRemotePath + " : " +
                     (mRemoteFolderChanged ? "changed" : "not changed"));
-            
+
         } else {
             // check failed
             if (result.getCode() == ResultCode.FILE_NOT_FOUND) {
                 removeLocalFolder();
             }
             if (result.isException()) {
-                Log_OC.e(TAG, "Checked " + mAccount.name + remotePath  + " : " + 
+                Log_OC.e(TAG, "Checked " + mAccount.name + mRemotePath  + " : " +
                         result.getLogMessage(), result.getException());
             } else {
-                Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + 
+                Log_OC.e(TAG, "Checked " + mAccount.name + mRemotePath + " : " +
                         result.getLogMessage());
             }
+
         }
-        
+
         return result;
     }
 
 
-    private RemoteOperationResult fetchAndSyncRemoteFolder(OwnCloudClient client) {
-        String remotePath = mLocalFolder.getRemotePath();
-        ReadRemoteFolderOperation operation = new ReadRemoteFolderOperation(remotePath);
-        RemoteOperationResult result = operation.execute(client);
-        Log_OC.d(TAG, "Synchronizing " + mAccount.name + remotePath);
+    private RemoteOperationResult fetchAndSyncRemoteFolder(OwnCloudClient client) throws OperationCancelledException {
+        if (mCancellationRequested.get()) {
+            throw new OperationCancelledException();
+        }
         
+        ReadRemoteFolderOperation operation = new ReadRemoteFolderOperation(mRemotePath);
+        RemoteOperationResult result = operation.execute(client);
+        Log_OC.d(TAG, "Synchronizing " + mAccount.name + mRemotePath);
+
         if (result.isSuccess()) {
             synchronizeData(result.getData(), client);
-            if (mConflictsFound > 0  || mFailsInFavouritesFound > 0) { 
-                result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);   
+            if (mConflictsFound > 0  || mFailsInFileSyncsFound > 0) {
+                result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);
                     // should be a different result code, but will do the job
             }
         } else {
@@ -293,17 +237,19 @@ public class SynchronizeFolderOperation extends RemoteOperation {
                 removeLocalFolder();
         }
         
+
         return result;
     }
 
-    
+
     private void removeLocalFolder() {
-        if (mStorageManager.fileExists(mLocalFolder.getFileId())) {
+        FileDataStorageManager storageManager = getStorageManager();
+        if (storageManager.fileExists(mLocalFolder.getFileId())) {
             String currentSavePath = FileStorageUtils.getSavePath(mAccount.name);
-            mStorageManager.removeFolder(
-                    mLocalFolder, 
-                    true, 
-                    (   mLocalFolder.isDown() && 
+            storageManager.removeFolder(
+                    mLocalFolder,
+                    true,
+                    (   mLocalFolder.isDown() &&        // TODO: debug, I think this is always false for folders
                             mLocalFolder.getStoragePath().startsWith(currentSavePath)
                     )
             );
@@ -312,50 +258,56 @@ public class SynchronizeFolderOperation extends RemoteOperation {
 
 
     /**
-     *  Synchronizes the data retrieved from the server about the contents of the target folder 
+     *  Synchronizes the data retrieved from the server about the contents of the target folder
      *  with the current data in the local database.
-     *  
+     *
      *  Grants that mChildren is updated with fresh data after execution.
-     *  
-     *  @param folderAndFiles   Remote folder and children files in Folder 
-     *  
-     *  @param client           Client instance to the remote server where the data were 
-     *                          retrieved.  
+     *
+     *  @param folderAndFiles   Remote folder and children files in Folder
+     *
+     *  @param client           Client instance to the remote server where the data were
+     *                          retrieved.
      *  @return                 'True' when any change was made in the local data, 'false' otherwise
      */
-    private void synchronizeData(ArrayList<Object> folderAndFiles, OwnCloudClient client) {
-        // get 'fresh data' from the database
-        mLocalFolder = mStorageManager.getFileByPath(mLocalFolder.getRemotePath());
-
-        // parse data from remote folder 
+    private void synchronizeData(ArrayList<Object> folderAndFiles, OwnCloudClient client)
+            throws OperationCancelledException {
+        FileDataStorageManager storageManager = getStorageManager();
+        
+        // parse data from remote folder
         OCFile remoteFolder = fillOCFile((RemoteFile)folderAndFiles.get(0));
         remoteFolder.setParentId(mLocalFolder.getParentId());
         remoteFolder.setFileId(mLocalFolder.getFileId());
-        
-        Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath() 
+
+        Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath()
                 + " changed - starting update of local data ");
-        
+
         List<OCFile> updatedFiles = new Vector<OCFile>(folderAndFiles.size() - 1);
-        List<SynchronizeFileOperation> filesToSyncContents = new Vector<SynchronizeFileOperation>();
+        mFilesForDirectDownload.clear();
+        mFilesToSyncContentsWithoutUpload.clear();
+        mFavouriteFilesToSyncContents.clear();
+
+        if (mCancellationRequested.get()) {
+            throw new OperationCancelledException();
+        }
 
         // get current data about local contents of the folder to synchronize
-        List<OCFile> localFiles = mStorageManager.getFolderContent(mLocalFolder);
+        List<OCFile> localFiles = storageManager.getFolderContent(mLocalFolder);
         Map<String, OCFile> localFilesMap = new HashMap<String, OCFile>(localFiles.size());
         for (OCFile file : localFiles) {
             localFilesMap.put(file.getRemotePath(), file);
         }
-        
-        // loop to update every child
+
+        // loop to synchronize every child
         OCFile remoteFile = null, localFile = null;
         for (int i=1; i<folderAndFiles.size(); i++) {
             /// new OCFile instance with the data from the server
             remoteFile = fillOCFile((RemoteFile)folderAndFiles.get(i));
             remoteFile.setParentId(mLocalFolder.getFileId());
 
-            /// retrieve local data for the read file 
+            /// retrieve local data for the read file
             //  localFile = mStorageManager.getFileByPath(remoteFile.getRemotePath());
             localFile = localFilesMap.remove(remoteFile.getRemotePath());
-            
+
             /// add to the remoteFile (the new one) data about LOCAL STATE (not existing in server)
             remoteFile.setLastSyncDateForProperties(mCurrentSyncTime);
             if (localFile != null) {
@@ -367,11 +319,11 @@ public class SynchronizeFolderOperation extends RemoteOperation {
                         localFile.getModificationTimestampAtLastSyncForData()
                 );
                 remoteFile.setStoragePath(localFile.getStoragePath());
-                // eTag will not be updated unless contents are synchronized 
+                // eTag will not be updated unless contents are synchronized
                 //  (Synchronize[File|Folder]Operation with remoteFile as parameter)
-                remoteFile.setEtag(localFile.getEtag());    
+                remoteFile.setEtag(localFile.getEtag());
                 if (remoteFile.isFolder()) {
-                    remoteFile.setFileLength(localFile.getFileLength()); 
+                    remoteFile.setFileLength(localFile.getFileLength());
                         // TODO move operations about size of folders to FileContentProvider
                 } else if (mRemoteFolderChanged && remoteFile.isImage() &&
                         remoteFile.getModificationTimestamp() != localFile.getModificationTimestamp()) {
@@ -381,81 +333,142 @@ public class SynchronizeFolderOperation extends RemoteOperation {
                 remoteFile.setPublicLink(localFile.getPublicLink());
                 remoteFile.setShareByLink(localFile.isShareByLink());
             } else {
-                // remote eTag will not be updated unless contents are synchronized 
+                // remote eTag will not be updated unless contents are synchronized
                 //  (Synchronize[File|Folder]Operation with remoteFile as parameter)
-                remoteFile.setEtag(""); 
+                remoteFile.setEtag("");
             }
 
             /// check and fix, if needed, local storage path
-            checkAndFixForeignStoragePath(remoteFile);      // policy - local files are COPIED 
-                                                            // into the ownCloud local folder;
-            searchForLocalFileInDefaultPath(remoteFile);    // legacy   
-
-            /// prepare content synchronization for kept-in-sync files
-            if (remoteFile.keepInSync()) {
-                SynchronizeFileOperation operation = new SynchronizeFileOperation(  localFile,        
-                                                                                    remoteFile, 
-                                                                                    mAccount, 
-                                                                                    true, 
-                                                                                    mContext
-                                                                                    );
+            searchForLocalFileInDefaultPath(remoteFile);
+            
+            /// classify file to sync/download contents later
+            if (remoteFile.isFolder()) {
+                /// to download children files recursively
+                synchronized(mCancellationRequested) {
+                    if (mCancellationRequested.get()) {
+                        throw new OperationCancelledException();
+                    }
+                    startSyncFolderOperation(remoteFile.getRemotePath());
+                }
+
+            } else if (remoteFile.keepInSync()) {
+                /// prepare content synchronization for kept-in-sync files
+                SynchronizeFileOperation operation = new SynchronizeFileOperation(
+                        localFile,
+                        remoteFile,
+                        mAccount,
+                        true,
+                        mContext
+                    );
+                mFavouriteFilesToSyncContents.add(operation);
                 
-                filesToSyncContents.add(operation);
+            } else {
+                /// prepare limited synchronization for regular files
+                SynchronizeFileOperation operation = new SynchronizeFileOperation(
+                        localFile,
+                        remoteFile,
+                        mAccount,
+                        true,
+                        false,
+                        mContext
+                    );
+                mFilesToSyncContentsWithoutUpload.add(operation);
             }
-            
+
             updatedFiles.add(remoteFile);
         }
 
         // save updated contents in local database
-        mStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
+        storageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
 
-        // request for the synchronization of file contents AFTER saving current remote properties
-        startContentSynchronizations(filesToSyncContents, client);
+    }
+    
+    
+    private void prepareOpsFromLocalKnowledge() throws OperationCancelledException {
+        List<OCFile> children = getStorageManager().getFolderContent(mLocalFolder);
+        for (OCFile child : children) {
+            /// classify file to sync/download contents later
+            if (child.isFolder()) {
+                /// to download children files recursively
+                synchronized(mCancellationRequested) {
+                    if (mCancellationRequested.get()) {
+                        throw new OperationCancelledException();
+                    }
+                    startSyncFolderOperation(child.getRemotePath());
+                }
 
-        mChildren = updatedFiles;
+            } else {
+                /// prepare limited synchronization for regular files
+                if (!child.isDown()) {
+                    mFilesForDirectDownload.add(child);
+                }
+            }
+        }
+    }
+
+
+    private void syncContents(OwnCloudClient client) throws OperationCancelledException {
+        startDirectDownloads();
+        startContentSynchronizations(mFilesToSyncContentsWithoutUpload, client);
+        startContentSynchronizations(mFavouriteFilesToSyncContents, client);
+    }
+
+    
+    private void startDirectDownloads() throws OperationCancelledException {
+        for (OCFile file : mFilesForDirectDownload) {
+            synchronized(mCancellationRequested) {
+                if (mCancellationRequested.get()) {
+                    throw new OperationCancelledException();
+                }
+                Intent i = new Intent(mContext, FileDownloader.class);
+                i.putExtra(FileDownloader.EXTRA_ACCOUNT, mAccount);
+                i.putExtra(FileDownloader.EXTRA_FILE, file);
+                mContext.startService(i);
+            }
+        }
     }
 
     /**
      * Performs a list of synchronization operations, determining if a download or upload is needed
      * or if exists conflict due to changes both in local and remote contents of the each file.
-     * 
-     * If download or upload is needed, request the operation to the corresponding service and goes 
+     *
+     * If download or upload is needed, request the operation to the corresponding service and goes
      * on.
-     * 
+     *
      * @param filesToSyncContents       Synchronization operations to execute.
      * @param client                    Interface to the remote ownCloud server.
      */
-    private void startContentSynchronizations(
-            List<SynchronizeFileOperation> filesToSyncContents, OwnCloudClient client
-        ) {
+    private void startContentSynchronizations(List<SyncOperation> filesToSyncContents, OwnCloudClient client) 
+            throws OperationCancelledException {
+        
         RemoteOperationResult contentsResult = null;
-        for (SynchronizeFileOperation op: filesToSyncContents) {
-            contentsResult = op.execute(mStorageManager, mContext);   // async
+        for (SyncOperation op: filesToSyncContents) {
+            if (mCancellationRequested.get()) {
+                throw new OperationCancelledException();
+            }
+            contentsResult = op.execute(getStorageManager(), mContext);
             if (!contentsResult.isSuccess()) {
                 if (contentsResult.getCode() == ResultCode.SYNC_CONFLICT) {
                     mConflictsFound++;
                 } else {
-                    mFailsInFavouritesFound++;
+                    mFailsInFileSyncsFound++;
                     if (contentsResult.getException() != null) {
-                        Log_OC.e(TAG, "Error while synchronizing favourites : " 
+                        Log_OC.e(TAG, "Error while synchronizing file : "
                                 +  contentsResult.getLogMessage(), contentsResult.getException());
                     } else {
-                        Log_OC.e(TAG, "Error while synchronizing favourites : " 
+                        Log_OC.e(TAG, "Error while synchronizing file : "
                                 + contentsResult.getLogMessage());
                     }
                 }
+                // TODO - use the errors count in notifications
             }   // won't let these fails break the synchronization process
         }
     }
 
-
-    public boolean isMultiStatus(int status) {
-        return (status == HttpStatus.SC_MULTI_STATUS); 
-    }
-
+    
     /**
-     * Creates and populates a new {@link OCFile} object with the data read from the server.
-     * 
+     * Creates and populates a new {@link com.owncloud.android.datamodel.OCFile} object with the data read from the server.
+     *
      * @param remote    remote file read from the server (remote file or folder).
      * @return          New OCFile instance representing the remote resource described by we.
      */
@@ -470,100 +483,11 @@ public class SynchronizeFolderOperation extends RemoteOperation {
         file.setRemoteId(remote.getRemoteId());
         return file;
     }
-    
 
-    /**
-     * Checks the storage path of the OCFile received as parameter. 
-     * If it's out of the local ownCloud folder, tries to copy the file inside it. 
-     * 
-     * If the copy fails, the link to the local file is nullified. The account of forgotten 
-     * files is kept in {@link #mForgottenLocalFiles}
-     *) 
-     * @param file      File to check and fix.
-     */
-    private void checkAndFixForeignStoragePath(OCFile file) {
-        String storagePath = file.getStoragePath();
-        String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, file);
-        if (storagePath != null && !storagePath.equals(expectedPath)) {
-            /// fix storagePaths out of the local ownCloud folder
-            File originalFile = new File(storagePath);
-            if (FileStorageUtils.getUsableSpace(mAccount.name) < originalFile.length()) {
-                mForgottenLocalFiles.put(file.getRemotePath(), storagePath);
-                file.setStoragePath(null);
-                    
-            } else {
-                InputStream in = null;
-                OutputStream out = null;
-                try {
-                    File expectedFile = new File(expectedPath);
-                    File expectedParent = expectedFile.getParentFile();
-                    expectedParent.mkdirs();
-                    if (!expectedParent.isDirectory()) {
-                        throw new IOException(
-                                "Unexpected error: parent directory could not be created"
-                        );
-                    }
-                    expectedFile.createNewFile();
-                    if (!expectedFile.isFile()) {
-                        throw new IOException("Unexpected error: target file could not be created");
-                    }                    
-                    in = new FileInputStream(originalFile);
-                    out = new FileOutputStream(expectedFile);
-                    byte[] buf = new byte[1024];
-                    int len;
-                    while ((len = in.read(buf)) > 0){
-                        out.write(buf, 0, len);
-                    }
-                    file.setStoragePath(expectedPath);
-                    
-                } catch (Exception e) {
-                    Log_OC.e(TAG, "Exception while copying foreign file " + expectedPath, e);
-                    mForgottenLocalFiles.put(file.getRemotePath(), storagePath);
-                    file.setStoragePath(null);
-                    
-                } finally {
-                    try {
-                        if (in != null) in.close();
-                    } catch (Exception e) {
-                        Log_OC.d(TAG, "Weird exception while closing input stream for " 
-                                + storagePath + " (ignoring)", e);
-                    }
-                    try {
-                        if (out != null) out.close();
-                    } catch (Exception e) {
-                        Log_OC.d(TAG, "Weird exception while closing output stream for " 
-                                + expectedPath + " (ignoring)", e);
-                    }
-                }
-            }
-        }
-    }
-    
-    
-    private RemoteOperationResult refreshSharesForFolder(OwnCloudClient client) {
-        RemoteOperationResult result = null;
-        
-        // remote request 
-        GetRemoteSharesForFileOperation operation = 
-                new GetRemoteSharesForFileOperation(mLocalFolder.getRemotePath(), false, true);
-        result = operation.execute(client);
-        
-        if (result.isSuccess()) {
-            // update local database
-            ArrayList<OCShare> shares = new ArrayList<OCShare>();
-            for(Object obj: result.getData()) {
-                shares.add((OCShare) obj);
-            }
-            mStorageManager.saveSharesInFolder(shares, mLocalFolder);
-        }
-
-        return result;
-    }
-    
 
     /**
      * Scans the default location for saving local copies of files searching for
-     * a 'lost' file with the same full name as the {@link OCFile} received as 
+     * a 'lost' file with the same full name as the {@link com.owncloud.android.datamodel.OCFile} received as
      * parameter.
      *  
      * @param file      File to associate a possible 'lost' local file.
@@ -580,31 +504,29 @@ public class SynchronizeFolderOperation extends RemoteOperation {
 
     
     /**
-     * Sends a message to any application component interested in the progress 
-     * of the synchronization.
-     * 
-     * @param event
-     * @param dirRemotePath     Remote path of a folder that was just synchronized 
-     *                          (with or without success)
-     * @param result
+     * Cancel operation
      */
-    private void sendLocalBroadcast(
-            String event, String dirRemotePath, RemoteOperationResult result
-        ) {
-        Log_OC.d(TAG, "Send broadcast " + event);
-        Intent intent = new Intent(event);
-        intent.putExtra(FileSyncAdapter.EXTRA_ACCOUNT_NAME, mAccount.name);
-        if (dirRemotePath != null) {
-            intent.putExtra(FileSyncAdapter.EXTRA_FOLDER_PATH, dirRemotePath);
-        }
-        intent.putExtra(FileSyncAdapter.EXTRA_RESULT, result);
-        mContext.sendStickyBroadcast(intent);
-        //LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
+    public void cancel() {
+        mCancellationRequested.set(true);
     }
 
+    public String getFolderPath() {
+        String path = mLocalFolder.getStoragePath();
+        if (path != null && path.length() > 0) {
+            return path;
+        }
+        return FileStorageUtils.getDefaultSavePathFor(mAccount.name, mLocalFolder);
+    }
 
-    public boolean getRemoteFolderChanged() {
-        return mRemoteFolderChanged;
+    private void startSyncFolderOperation(String path){
+        Intent intent = new Intent(mContext, OperationsService.class);
+        intent.setAction(OperationsService.ACTION_SYNC_FOLDER);
+        intent.putExtra(OperationsService.EXTRA_ACCOUNT, mAccount);
+        intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, path);
+        mContext.startService(intent);
     }
 
+    public String getRemotePath() {
+        return mRemotePath;
+    }
 }

+ 23 - 2
src/com/owncloud/android/providers/FileContentProvider.java

@@ -97,6 +97,8 @@ public class FileContentProvider extends ContentProvider {
                 ProviderTableMeta.FILE_REMOTE_ID);
         mFileProjectionMap.put(ProviderTableMeta.FILE_UPDATE_THUMBNAIL,
                 ProviderTableMeta.FILE_UPDATE_THUMBNAIL);
+        mFileProjectionMap.put(ProviderTableMeta.FILE_IS_DOWNLOADING,
+                ProviderTableMeta.FILE_IS_DOWNLOADING);
     }
 
     private static final int SINGLE_FILE = 1;
@@ -624,7 +626,8 @@ public class FileContentProvider extends ContentProvider {
                     + ProviderTableMeta.FILE_PUBLIC_LINK  + " TEXT, "
                     + ProviderTableMeta.FILE_PERMISSIONS  + " TEXT null,"
                     + ProviderTableMeta.FILE_REMOTE_ID  + " TEXT null,"
-                    + ProviderTableMeta.FILE_UPDATE_THUMBNAIL  + " INTEGER);" //boolean
+                    + ProviderTableMeta.FILE_UPDATE_THUMBNAIL  + " INTEGER," //boolean
+                    + ProviderTableMeta.FILE_IS_DOWNLOADING  + " INTEGER);" //boolean
                     );
             
             // Create table ocshares
@@ -795,7 +798,25 @@ public class FileContentProvider extends ContentProvider {
                 }
             }
             if (!upgraded)
-                Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + 
+                Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion +
+                        ", newVersion == " + newVersion);
+
+            if (oldVersion < 9 && newVersion >= 9) {
+                Log_OC.i("SQL", "Entering in the #9 ADD in onUpgrade");
+                db.beginTransaction();
+                try {
+                    db .execSQL("ALTER TABLE " + ProviderTableMeta.FILE_TABLE_NAME +
+                            " ADD COLUMN " + ProviderTableMeta.FILE_IS_DOWNLOADING + " INTEGER " +
+                            " DEFAULT 0");
+
+                    upgraded = true;
+                    db.setTransactionSuccessful();
+                } finally {
+                    db.endTransaction();
+                }
+            }
+            if (!upgraded)
+                Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion +
                         ", newVersion == " + newVersion);
         }
     }

+ 341 - 240
src/com/owncloud/android/services/OperationsService.java

@@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentMap;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.FileDataStorageManager;
+import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
@@ -48,6 +49,7 @@ import com.owncloud.android.operations.OAuth2GetAccessToken;
 import com.owncloud.android.operations.RemoveFileOperation;
 import com.owncloud.android.operations.RenameFileOperation;
 import com.owncloud.android.operations.SynchronizeFileOperation;
+import com.owncloud.android.operations.SynchronizeFolderOperation;
 import com.owncloud.android.operations.UnshareLinkOperation;
 
 import android.accounts.Account;
@@ -81,7 +83,8 @@ public class OperationsService extends Service {
     public static final String EXTRA_SYNC_FILE_CONTENTS = "SYNC_FILE_CONTENTS";
     public static final String EXTRA_RESULT = "RESULT";
     public static final String EXTRA_NEW_PARENT_PATH = "NEW_PARENT_PATH";
-    
+    public static final String EXTRA_FILE = "FILE";
+
     // TODO review if ALL OF THEM are necessary
     public static final String EXTRA_SUCCESS_IF_ABSENT = "SUCCESS_IF_ABSENT";
     public static final String EXTRA_USERNAME = "USERNAME";
@@ -99,13 +102,13 @@ public class OperationsService extends Service {
     public static final String ACTION_REMOVE = "REMOVE";
     public static final String ACTION_CREATE_FOLDER = "CREATE_FOLDER";
     public static final String ACTION_SYNC_FILE = "SYNC_FILE";
+    public static final String ACTION_SYNC_FOLDER = "SYNC_FOLDER";  // for the moment, just to download
+    //public static final String ACTION_CANCEL_SYNC_FOLDER = "CANCEL_SYNC_FOLDER";  // for the moment, just to download
     public static final String ACTION_MOVE_FILE = "MOVE_FILE";
     
     public static final String ACTION_OPERATION_ADDED = OperationsService.class.getName() + ".OPERATION_ADDED";
     public static final String ACTION_OPERATION_FINISHED = OperationsService.class.getName() + ".OPERATION_FINISHED";
 
-    private ConcurrentLinkedQueue<Pair<Target, RemoteOperation>> mPendingOperations = 
-            new ConcurrentLinkedQueue<Pair<Target, RemoteOperation>>();
 
     private ConcurrentMap<Integer, Pair<RemoteOperation, RemoteOperationResult>> 
         mUndispatchedFinishedOperations =
@@ -130,14 +133,10 @@ public class OperationsService extends Service {
         }
     }
 
-    private Looper mServiceLooper;
-    private ServiceHandler mServiceHandler;
-    private OperationsServiceBinder mBinder;
-    private OwnCloudClient mOwnCloudClient = null;
-    private Target mLastTarget = null;
-    private FileDataStorageManager mStorageManager;
-    private RemoteOperation mCurrentOperation = null;
+    private ServiceHandler mOperationsHandler;
+    private OperationsServiceBinder mOperationsBinder;
     
+    private SyncFolderHandler mSyncFolderHandler;
     
     /**
      * Service initialization
@@ -145,11 +144,16 @@ public class OperationsService extends Service {
     @Override
     public void onCreate() {
         super.onCreate();
-        HandlerThread thread = new HandlerThread("Operations service thread", Process.THREAD_PRIORITY_BACKGROUND);
+        /// First worker thread for most of operations 
+        HandlerThread thread = new HandlerThread("Operations thread", Process.THREAD_PRIORITY_BACKGROUND);
         thread.start();
-        mServiceLooper = thread.getLooper();
-        mServiceHandler = new ServiceHandler(mServiceLooper, this);
-        mBinder = new OperationsServiceBinder();
+        mOperationsHandler = new ServiceHandler(thread.getLooper(), this);
+        mOperationsBinder = new OperationsServiceBinder(mOperationsHandler);
+        
+        /// Separated worker thread for download of folders (WIP)
+        thread = new HandlerThread("Syncfolder thread", Process.THREAD_PRIORITY_BACKGROUND);
+        thread.start();
+        mSyncFolderHandler = new SyncFolderHandler(thread.getLooper(), this);
     }
 
     
@@ -158,17 +162,43 @@ public class OperationsService extends Service {
      * 
      * New operations are added calling to startService(), resulting in a call to this method. 
      * This ensures the service will keep on working although the caller activity goes away.
-     * 
-     * IMPORTANT: the only operations performed here right now is {@link GetSharedFilesOperation}. The class
-     * is taking advantage of it due to time constraints.
      */
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
-        //Log_OC.wtf(TAG, "onStartCommand init" );
-        Message msg = mServiceHandler.obtainMessage();
-        msg.arg1 = startId;
-        mServiceHandler.sendMessage(msg);
-        //Log_OC.wtf(TAG, "onStartCommand end" );
+        // WIP: for the moment, only SYNC_FOLDER and CANCEL_SYNC_FOLDER is expected here;
+        // the rest of the operations are requested through the Binder
+        if (ACTION_SYNC_FOLDER.equals(intent.getAction())) {
+
+            /*Log_OC.v("NOW " + TAG + ", thread " + Thread.currentThread().getName(), "Received request to sync folder");*/
+
+            if (!intent.hasExtra(EXTRA_ACCOUNT) || !intent.hasExtra(EXTRA_REMOTE_PATH)) {
+                Log_OC.e(TAG, "Not enough information provided in intent");
+                return START_NOT_STICKY;
+            }
+            Account account = intent.getParcelableExtra(EXTRA_ACCOUNT);
+            String remotePath = intent.getStringExtra(EXTRA_REMOTE_PATH);
+
+            Pair<Account, String> itemSyncKey =  new Pair<Account , String>(account, remotePath);
+
+            Pair<Target, RemoteOperation> itemToQueue = newOperation(intent);
+            if (itemToQueue != null) {
+                mSyncFolderHandler.add(account, remotePath, (SynchronizeFolderOperation)itemToQueue.second);
+                Message msg = mSyncFolderHandler.obtainMessage();
+                msg.arg1 = startId;
+                msg.obj = itemSyncKey;
+                /*Log_OC.v(
+                        "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                        "Sync folder " + remotePath + " added to queue"
+                );*/
+                mSyncFolderHandler.sendMessage(msg);
+            }
+
+        } else {
+            Message msg = mOperationsHandler.obtainMessage();
+            msg.arg1 = startId;
+            mOperationsHandler.sendMessage(msg);
+        }
+        
         return START_NOT_STICKY;
     }
 
@@ -191,14 +221,13 @@ public class OperationsService extends Service {
             e.printStackTrace();
         }
         
-        //Log_OC.wtf(TAG, "Clear mUndispatchedFinisiedOperations" );
+        //Log_OC.wtf(TAG, "Clear mUndispatchedFinishedOperations" );
         mUndispatchedFinishedOperations.clear();
         
         //Log_OC.wtf(TAG, "onDestroy end" );
         super.onDestroy();
     }
 
-
     /**
      * Provides a binder object that clients can use to perform actions on the queue of operations, 
      * except the addition of new operations. 
@@ -206,7 +235,7 @@ public class OperationsService extends Service {
     @Override
     public IBinder onBind(Intent intent) {
         //Log_OC.wtf(TAG, "onBind" );
-        return mBinder;
+        return mOperationsBinder;
     }
 
     
@@ -215,11 +244,11 @@ public class OperationsService extends Service {
      */
     @Override
     public boolean onUnbind(Intent intent) {
-        ((OperationsServiceBinder)mBinder).clearListeners();
+        ((OperationsServiceBinder)mOperationsBinder).clearListeners();
         return false;   // not accepting rebinding (default behaviour)
     }
 
-    
+
     /**
      *  Binder to let client components to perform actions on the queue of operations.
      * 
@@ -233,16 +262,28 @@ public class OperationsService extends Service {
         private ConcurrentMap<OnRemoteOperationListener, Handler> mBoundListeners = 
                 new ConcurrentHashMap<OnRemoteOperationListener, Handler>();
         
+        private ServiceHandler mServiceHandler = null;   
+
+        public OperationsServiceBinder(ServiceHandler serviceHandler) {
+            mServiceHandler = serviceHandler;
+        }
+
+
         /**
-         * Cancels an operation
+         * Cancels a pending or current synchronization.
          *
-         * TODO
+         * @param account       ownCloud account where the remote folder is stored.
+         * @param file          A folder in the queue of pending synchronizations
          */
-        public void cancel() {
-            // TODO
+        public void cancel(Account account, OCFile file) {
+            /*Log_OC.v(
+                    "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                    "Received request to cancel folder " + file.getRemotePath()
+            );*/
+            mSyncFolderHandler.cancel(account, file);
         }
-        
-        
+
+
         public void clearListeners() {
             
             mBoundListeners.clear();
@@ -280,131 +321,31 @@ public class OperationsService extends Service {
          * @return  'True' when an operation that enforces the user to wait for completion is in process.
          */
         public boolean isPerformingBlockingOperation() {
-            return (!mPendingOperations.isEmpty());
+            return (!mServiceHandler.mPendingOperations.isEmpty());
         }
 
 
         /**
-         * Creates and adds to the queue a new operation, as described by operationIntent
+         * Creates and adds to the queue a new operation, as described by operationIntent.
+         * 
+         * Calls startService to make the operation is processed by the ServiceHandler.
          * 
          * @param operationIntent       Intent describing a new operation to queue and execute.
          * @return                      Identifier of the operation created, or null if failed.
          */
-        public long newOperation(Intent operationIntent) {
-            RemoteOperation operation = null;
-            Target target = null;
-            try {
-                if (!operationIntent.hasExtra(EXTRA_ACCOUNT) && 
-                        !operationIntent.hasExtra(EXTRA_SERVER_URL)) {
-                    Log_OC.e(TAG, "Not enough information provided in intent");
-                    
-                } else {
-                    Account account = operationIntent.getParcelableExtra(EXTRA_ACCOUNT);
-                    String serverUrl = operationIntent.getStringExtra(EXTRA_SERVER_URL);
-                    String username = operationIntent.getStringExtra(EXTRA_USERNAME);
-                    String password = operationIntent.getStringExtra(EXTRA_PASSWORD);
-                    String authToken = operationIntent.getStringExtra(EXTRA_AUTH_TOKEN);
-                    String cookie = operationIntent.getStringExtra(EXTRA_COOKIE);
-                    target = new Target(
-                            account, 
-                            (serverUrl == null) ? null : Uri.parse(serverUrl),
-                            username,
-                            password,
-                            authToken,
-                            cookie
-                    );
-                    
-                    String action = operationIntent.getAction();
-                    if (action.equals(ACTION_CREATE_SHARE)) {  // Create Share
-                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
-                        Intent sendIntent = operationIntent.getParcelableExtra(EXTRA_SEND_INTENT);
-                        if (remotePath.length() > 0) {
-                            operation = new CreateShareOperation(OperationsService.this, remotePath, ShareType.PUBLIC_LINK,
-                                    "", false, "", 1, sendIntent);
-                        }
-                        
-                    } else if (action.equals(ACTION_UNSHARE)) {  // Unshare file
-                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
-                        if (remotePath.length() > 0) {
-                            operation = new UnshareLinkOperation(
-                                    remotePath, 
-                                    OperationsService.this);
-                        }
-                        
-                    } else if (action.equals(ACTION_GET_SERVER_INFO)) { 
-                        // check OC server and get basic information from it
-                        operation = new GetServerInfoOperation(serverUrl, OperationsService.this);
-                        
-                    } else if (action.equals(ACTION_OAUTH2_GET_ACCESS_TOKEN)) {
-                        /// GET ACCESS TOKEN to the OAuth server
-                        String oauth2QueryParameters =
-                                operationIntent.getStringExtra(EXTRA_OAUTH2_QUERY_PARAMETERS);
-                        operation = new OAuth2GetAccessToken(
-                                getString(R.string.oauth2_client_id), 
-                                getString(R.string.oauth2_redirect_uri),       
-                                getString(R.string.oauth2_grant_type),
-                                oauth2QueryParameters);
-                        
-                    } else if (action.equals(ACTION_EXISTENCE_CHECK)) {
-                        // Existence Check 
-                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
-                        boolean successIfAbsent = operationIntent.getBooleanExtra(EXTRA_SUCCESS_IF_ABSENT, false);
-                        operation = new ExistenceCheckRemoteOperation(remotePath, OperationsService.this, successIfAbsent);
-                        
-                    } else if (action.equals(ACTION_GET_USER_NAME)) {
-                        // Get User Name
-                        operation = new GetRemoteUserNameOperation();
-                        
-                    } else if (action.equals(ACTION_RENAME)) {
-                        // Rename file or folder
-                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
-                        String newName = operationIntent.getStringExtra(EXTRA_NEWNAME);
-                        operation = new RenameFileOperation(remotePath, newName);
-                        
-                    } else if (action.equals(ACTION_REMOVE)) {
-                        // Remove file or folder
-                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
-                        boolean onlyLocalCopy = operationIntent.getBooleanExtra(EXTRA_REMOVE_ONLY_LOCAL, false);
-                        operation = new RemoveFileOperation(remotePath, onlyLocalCopy);
-                        
-                    } else if (action.equals(ACTION_CREATE_FOLDER)) {
-                        // Create Folder
-                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
-                        boolean createFullPath = operationIntent.getBooleanExtra(EXTRA_CREATE_FULL_PATH, true);
-                        operation = new CreateFolderOperation(remotePath, createFullPath);
-                        
-                    } else if (action.equals(ACTION_SYNC_FILE)) {
-                        // Sync file
-                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
-                        boolean syncFileContents = operationIntent.getBooleanExtra(EXTRA_SYNC_FILE_CONTENTS, true);
-                        operation = new SynchronizeFileOperation(remotePath, account, syncFileContents, getApplicationContext());
-                    } else if (action.equals(ACTION_MOVE_FILE)) {
-                        // Move file/folder
-                        String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
-                        String newParentPath = operationIntent.getStringExtra(EXTRA_NEW_PARENT_PATH);
-                        operation = new MoveFileOperation(remotePath,newParentPath,account);
-                    }
-                    
-                }
-                    
-            } catch (IllegalArgumentException e) {
-                Log_OC.e(TAG, "Bad information provided in intent: " + e.getMessage());
-                operation = null;
-            }
-
-            if (operation != null) {
-                mPendingOperations.add(new Pair<Target , RemoteOperation>(target, operation));
+        public long queueNewOperation(Intent operationIntent) {
+            Pair<Target, RemoteOperation> itemToQueue = newOperation(operationIntent);
+            if (itemToQueue != null) {
+                mServiceHandler.mPendingOperations.add(itemToQueue);
                 startService(new Intent(OperationsService.this, OperationsService.class));
-                //Log_OC.wtf(TAG, "New operation added, opId: " + operation.hashCode());
-                // better id than hash? ; should be good enough by the time being
-                return operation.hashCode();
+                return itemToQueue.second.hashCode();
                 
             } else {
-                //Log_OC.wtf(TAG, "New operation failed, returned Long.MAX_VALUE");
                 return Long.MAX_VALUE;
             }
         }
-
+        
+        
         public boolean dispatchResultIfFinished(int operationId, OnRemoteOperationListener listener) {
             Pair<RemoteOperation, RemoteOperationResult> undispatched = 
                     mUndispatchedFinishedOperations.remove(operationId);
@@ -413,7 +354,7 @@ public class OperationsService extends Service {
                 return true;
                 //Log_OC.wtf(TAG, "Sending callback later");
             } else {
-                if (!mPendingOperations.isEmpty()) {
+                if (!mServiceHandler.mPendingOperations.isEmpty()) {
                     return true;
                 } else {
                     return false;
@@ -421,18 +362,46 @@ public class OperationsService extends Service {
                 //Log_OC.wtf(TAG, "Not finished yet");
             }
         }
+        
+        
+        /**
+         * Returns True when the file described by 'file' in the ownCloud account 'account' is downloading or waiting
+         * to download.
+         * 
+         * If 'file' is a directory, returns 'true' if some of its descendant files is downloading or waiting
+         * to download.
+         * 
+         * @param account       ownCloud account where the remote file is stored.
+         * @param remotePath    Path of the folder to check if something is synchronizing / downloading / uploading
+         *                      inside.
+         */
+        public boolean isSynchronizing(Account account, String remotePath) {
+            return mSyncFolderHandler.isSynchronizing(account, remotePath);
+        }
 
     }
-    
-    
-    /** 
+
+
+    /**
      * Operations worker. Performs the pending operations in the order they were requested. 
      * 
      * Created with the Looper of a new thread, started in {@link OperationsService#onCreate()}. 
      */
     private static class ServiceHandler extends Handler {
         // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak
+        
+        
         OperationsService mService;
+        
+        
+        private ConcurrentLinkedQueue<Pair<Target, RemoteOperation>> mPendingOperations =
+                new ConcurrentLinkedQueue<Pair<Target, RemoteOperation>>();
+        private RemoteOperation mCurrentOperation = null;
+        private Target mLastTarget = null;
+        private OwnCloudClient mOwnCloudClient = null;
+        private FileDataStorageManager mStorageManager;
+        
+        
         public ServiceHandler(Looper looper, OperationsService service) {
             super(looper);
             if (service == null) {
@@ -443,107 +412,241 @@ public class OperationsService extends Service {
 
         @Override
         public void handleMessage(Message msg) {
-            mService.nextOperation();
+            nextOperation();
             mService.stopSelf(msg.arg1);
         }
-    }
-    
-
-    /**
-     * Performs the next operation in the queue
-     */
-    private void nextOperation() {
         
-        //Log_OC.wtf(TAG, "nextOperation init" );
         
-        Pair<Target, RemoteOperation> next = null;
-        synchronized(mPendingOperations) {
-            next = mPendingOperations.peek();
-        }
-
-        if (next != null) {
+        /**
+         * Performs the next operation in the queue
+         */
+        private void nextOperation() {
             
-            mCurrentOperation = next.second;
-            RemoteOperationResult result = null;
-            try {
-                /// prepare client object to send the request to the ownCloud server
-                if (mLastTarget == null || !mLastTarget.equals(next.first)) {
-                    mLastTarget = next.first;
-                    if (mLastTarget.mAccount != null) {
-                        OwnCloudAccount ocAccount = new OwnCloudAccount(mLastTarget.mAccount, this);
-                        mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton().
-                                getClientFor(ocAccount, this);
-                        mStorageManager = 
-                                new FileDataStorageManager(
-                                        mLastTarget.mAccount, 
-                                        getContentResolver());
-                    } else {
-                        OwnCloudCredentials credentials = null;
-                        if (mLastTarget.mUsername != null && 
-                                mLastTarget.mUsername.length() > 0) {
-                            credentials = OwnCloudCredentialsFactory.newBasicCredentials(
-                                    mLastTarget.mUsername, 
-                                    mLastTarget.mPassword);  // basic
-                            
-                        } else if (mLastTarget.mAuthToken != null && 
-                                mLastTarget.mAuthToken.length() > 0) {
-                            credentials = OwnCloudCredentialsFactory.newBearerCredentials(
-                                    mLastTarget.mAuthToken);  // bearer token
-                            
-                        } else if (mLastTarget.mCookie != null &&
-                                mLastTarget.mCookie.length() > 0) {
-                            credentials = OwnCloudCredentialsFactory.newSamlSsoCredentials(
-                                    mLastTarget.mCookie); // SAML SSO
+            //Log_OC.wtf(TAG, "nextOperation init" );
+            
+            Pair<Target, RemoteOperation> next = null;
+            synchronized(mPendingOperations) {
+                next = mPendingOperations.peek();
+            }
+
+            if (next != null) {
+                
+                mCurrentOperation = next.second;
+                RemoteOperationResult result = null;
+                try {
+                    /// prepare client object to send the request to the ownCloud server
+                    if (mLastTarget == null || !mLastTarget.equals(next.first)) {
+                        mLastTarget = next.first;
+                        if (mLastTarget.mAccount != null) {
+                            OwnCloudAccount ocAccount = new OwnCloudAccount(mLastTarget.mAccount, mService);
+                            mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton().
+                                    getClientFor(ocAccount, mService);
+                            mStorageManager = new FileDataStorageManager(
+                                    mLastTarget.mAccount, 
+                                    mService.getContentResolver()
+                            );
+                        } else {
+                            OwnCloudCredentials credentials = null;
+                            if (mLastTarget.mUsername != null && 
+                                    mLastTarget.mUsername.length() > 0) {
+                                credentials = OwnCloudCredentialsFactory.newBasicCredentials(
+                                        mLastTarget.mUsername, 
+                                        mLastTarget.mPassword);  // basic
+                                
+                            } else if (mLastTarget.mAuthToken != null && 
+                                    mLastTarget.mAuthToken.length() > 0) {
+                                credentials = OwnCloudCredentialsFactory.newBearerCredentials(
+                                        mLastTarget.mAuthToken);  // bearer token
+                                
+                            } else if (mLastTarget.mCookie != null &&
+                                    mLastTarget.mCookie.length() > 0) {
+                                credentials = OwnCloudCredentialsFactory.newSamlSsoCredentials(
+                                        mLastTarget.mCookie); // SAML SSO
+                            }
+                            OwnCloudAccount ocAccount = new OwnCloudAccount(
+                                    mLastTarget.mServerUrl, credentials);
+                            mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton().
+                                    getClientFor(ocAccount, mService);
+                            mStorageManager = null;
                         }
-                        OwnCloudAccount ocAccount = new OwnCloudAccount(
-                                mLastTarget.mServerUrl, credentials);
-                        mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton().
-                                getClientFor(ocAccount, this);
-                        mStorageManager = null;
                     }
-                }
 
-                /// perform the operation
-                if (mCurrentOperation instanceof SyncOperation) {
-                    result = ((SyncOperation)mCurrentOperation).execute(mOwnCloudClient, mStorageManager);
-                } else {
-                    result = mCurrentOperation.execute(mOwnCloudClient);
-                }
+                    /// perform the operation
+                    if (mCurrentOperation instanceof SyncOperation) {
+                        result = ((SyncOperation)mCurrentOperation).execute(mOwnCloudClient, mStorageManager);
+                    } else {
+                        result = mCurrentOperation.execute(mOwnCloudClient);
+                    }
+                    
+                } catch (AccountsException e) {
+                    if (mLastTarget.mAccount == null) {
+                        Log_OC.e(TAG, "Error while trying to get authorization for a NULL account", e);
+                    } else {
+                        Log_OC.e(TAG, "Error while trying to get authorization for " + mLastTarget.mAccount.name, e);
+                    }
+                    result = new RemoteOperationResult(e);
+                    
+                } catch (IOException e) {
+                    if (mLastTarget.mAccount == null) {
+                        Log_OC.e(TAG, "Error while trying to get authorization for a NULL account", e);
+                    } else {
+                        Log_OC.e(TAG, "Error while trying to get authorization for " + mLastTarget.mAccount.name, e);
+                    }
+                    result = new RemoteOperationResult(e);
+                } catch (Exception e) {
+                    if (mLastTarget.mAccount == null) {
+                        Log_OC.e(TAG, "Unexpected error for a NULL account", e);
+                    } else {
+                        Log_OC.e(TAG, "Unexpected error for " + mLastTarget.mAccount.name, e);
+                    }
+                    result = new RemoteOperationResult(e);
                 
-            } catch (AccountsException e) {
-                if (mLastTarget.mAccount == null) {
-                    Log_OC.e(TAG, "Error while trying to get authorization for a NULL account", e);
-                } else {
-                    Log_OC.e(TAG, "Error while trying to get authorization for " + mLastTarget.mAccount.name, e);
+                } finally {
+                    synchronized(mPendingOperations) {
+                        mPendingOperations.poll();
+                    }
                 }
-                result = new RemoteOperationResult(e);
                 
-            } catch (IOException e) {
-                if (mLastTarget.mAccount == null) {
-                    Log_OC.e(TAG, "Error while trying to get authorization for a NULL account", e);
-                } else {
-                    Log_OC.e(TAG, "Error while trying to get authorization for " + mLastTarget.mAccount.name, e);
-                }
-                result = new RemoteOperationResult(e);
-            } catch (Exception e) {
-                if (mLastTarget.mAccount == null) {
-                    Log_OC.e(TAG, "Unexpected error for a NULL account", e);
-                } else {
-                    Log_OC.e(TAG, "Unexpected error for " + mLastTarget.mAccount.name, e);
-                }
-                result = new RemoteOperationResult(e);
-            
-            } finally {
-                synchronized(mPendingOperations) {
-                    mPendingOperations.poll();
-                }
+                //sendBroadcastOperationFinished(mLastTarget, mCurrentOperation, result);
+                mService.dispatchResultToOperationListeners(mLastTarget, mCurrentOperation, result);
             }
-            
-            //sendBroadcastOperationFinished(mLastTarget, mCurrentOperation, result);
-            dispatchResultToOperationListeners(mLastTarget, mCurrentOperation, result);
         }
+
+
+        
     }
+    
 
+    /**
+     * Creates a new operation, as described by operationIntent.
+     * 
+     * TODO - move to ServiceHandler (probably)
+     * 
+     * @param operationIntent       Intent describing a new operation to queue and execute.
+     * @return                      Pair with the new operation object and the information about its target server.
+     */
+    private Pair<Target , RemoteOperation> newOperation(Intent operationIntent) {
+        RemoteOperation operation = null;
+        Target target = null;
+        try {
+            if (!operationIntent.hasExtra(EXTRA_ACCOUNT) && 
+                    !operationIntent.hasExtra(EXTRA_SERVER_URL)) {
+                Log_OC.e(TAG, "Not enough information provided in intent");
+                
+            } else {
+                Account account = operationIntent.getParcelableExtra(EXTRA_ACCOUNT);
+                String serverUrl = operationIntent.getStringExtra(EXTRA_SERVER_URL);
+                String username = operationIntent.getStringExtra(EXTRA_USERNAME);
+                String password = operationIntent.getStringExtra(EXTRA_PASSWORD);
+                String authToken = operationIntent.getStringExtra(EXTRA_AUTH_TOKEN);
+                String cookie = operationIntent.getStringExtra(EXTRA_COOKIE);
+                target = new Target(
+                        account, 
+                        (serverUrl == null) ? null : Uri.parse(serverUrl),
+                        username,
+                        password,
+                        authToken,
+                        cookie
+                );
+                
+                String action = operationIntent.getAction();
+                if (action.equals(ACTION_CREATE_SHARE)) {  // Create Share
+                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
+                    Intent sendIntent = operationIntent.getParcelableExtra(EXTRA_SEND_INTENT);
+                    if (remotePath.length() > 0) {
+                        operation = new CreateShareOperation(OperationsService.this, remotePath, ShareType.PUBLIC_LINK,
+                                "", false, "", 1, sendIntent);
+                    }
+                    
+                } else if (action.equals(ACTION_UNSHARE)) {  // Unshare file
+                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
+                    if (remotePath.length() > 0) {
+                        operation = new UnshareLinkOperation(
+                                remotePath, 
+                                OperationsService.this);
+                    }
+                    
+                } else if (action.equals(ACTION_GET_SERVER_INFO)) { 
+                    // check OC server and get basic information from it
+                    operation = new GetServerInfoOperation(serverUrl, OperationsService.this);
+                    
+                } else if (action.equals(ACTION_OAUTH2_GET_ACCESS_TOKEN)) {
+                    /// GET ACCESS TOKEN to the OAuth server
+                    String oauth2QueryParameters =
+                            operationIntent.getStringExtra(EXTRA_OAUTH2_QUERY_PARAMETERS);
+                    operation = new OAuth2GetAccessToken(
+                            getString(R.string.oauth2_client_id), 
+                            getString(R.string.oauth2_redirect_uri),       
+                            getString(R.string.oauth2_grant_type),
+                            oauth2QueryParameters);
+                    
+                } else if (action.equals(ACTION_EXISTENCE_CHECK)) {
+                    // Existence Check 
+                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
+                    boolean successIfAbsent = operationIntent.getBooleanExtra(EXTRA_SUCCESS_IF_ABSENT, false);
+                    operation = new ExistenceCheckRemoteOperation(remotePath, OperationsService.this, successIfAbsent);
+                    
+                } else if (action.equals(ACTION_GET_USER_NAME)) {
+                    // Get User Name
+                    operation = new GetRemoteUserNameOperation();
+                    
+                } else if (action.equals(ACTION_RENAME)) {
+                    // Rename file or folder
+                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
+                    String newName = operationIntent.getStringExtra(EXTRA_NEWNAME);
+                    operation = new RenameFileOperation(remotePath, newName);
+                    
+                } else if (action.equals(ACTION_REMOVE)) {
+                    // Remove file or folder
+                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
+                    boolean onlyLocalCopy = operationIntent.getBooleanExtra(EXTRA_REMOVE_ONLY_LOCAL, false);
+                    operation = new RemoveFileOperation(remotePath, onlyLocalCopy);
+                    
+                } else if (action.equals(ACTION_CREATE_FOLDER)) {
+                    // Create Folder
+                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
+                    boolean createFullPath = operationIntent.getBooleanExtra(EXTRA_CREATE_FULL_PATH, true);
+                    operation = new CreateFolderOperation(remotePath, createFullPath);
+                    
+                } else if (action.equals(ACTION_SYNC_FILE)) {
+                    // Sync file
+                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
+                    boolean syncFileContents = operationIntent.getBooleanExtra(EXTRA_SYNC_FILE_CONTENTS, true);
+                    operation = new SynchronizeFileOperation(
+                            remotePath, account, syncFileContents, getApplicationContext()
+                    );
+                    
+                } else if (action.equals(ACTION_SYNC_FOLDER)) {
+                    // Sync file
+                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
+                    operation = new SynchronizeFolderOperation(
+                            this,                       // TODO remove this dependency from construction time 
+                            remotePath,
+                            account, 
+                            System.currentTimeMillis()  // TODO remove this dependency from construction time
+                    );
+                    
+                } else if (action.equals(ACTION_MOVE_FILE)) {
+                    // Move file/folder
+                    String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
+                    String newParentPath = operationIntent.getStringExtra(EXTRA_NEW_PARENT_PATH);
+                    operation = new MoveFileOperation(remotePath,newParentPath,account);
+                }
+                
+            }
+                
+        } catch (IllegalArgumentException e) {
+            Log_OC.e(TAG, "Bad information provided in intent: " + e.getMessage());
+            operation = null;
+        }
+
+        if (operation != null) {
+            return new Pair<Target , RemoteOperation>(target, operation);  
+        } else {
+            return null;
+        }
+    }
+    
 
     /**
      * Sends a broadcast when a new operation is added to the queue.
@@ -593,18 +696,18 @@ public class OperationsService extends Service {
     
     /**
      * Notifies the currently subscribed listeners about the end of an operation.
-     * 
+     *
      * @param target            Account or URL pointing to an OC server.
      * @param operation         Finished operation.
      * @param result            Result of the operation.
      */
-    private void dispatchResultToOperationListeners(
+    protected void dispatchResultToOperationListeners(
             Target target, final RemoteOperation operation, final RemoteOperationResult result) {
         int count = 0;
-        Iterator<OnRemoteOperationListener> listeners = mBinder.mBoundListeners.keySet().iterator();
+        Iterator<OnRemoteOperationListener> listeners = mOperationsBinder.mBoundListeners.keySet().iterator();
         while (listeners.hasNext()) {
             final OnRemoteOperationListener listener = listeners.next();
-            final Handler handler = mBinder.mBoundListeners.get(listener);
+            final Handler handler = mOperationsBinder.mBoundListeners.get(listener);
             if (handler != null) { 
                 handler.post(new Runnable() {
                     @Override
@@ -623,6 +726,4 @@ public class OperationsService extends Service {
         }
         Log_OC.d(TAG, "Called " + count + " listeners");
     }
-    
-
 }

+ 213 - 0
src/com/owncloud/android/services/SyncFolderHandler.java

@@ -0,0 +1,213 @@
+/* ownCloud Android client application
+ *   Copyright (C) 2015 ownCloud Inc.
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License version 2,
+ *   as published by the Free Software Foundation.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.owncloud.android.services;
+
+import android.accounts.Account;
+import android.accounts.AccountsException;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Pair;
+
+import com.owncloud.android.datamodel.FileDataStorageManager;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.files.services.FileDownloader;
+import com.owncloud.android.files.services.IndexedForest;
+import com.owncloud.android.lib.common.OwnCloudAccount;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.operations.SynchronizeFolderOperation;
+import com.owncloud.android.utils.FileStorageUtils;
+
+import java.io.IOException;
+
+/**
+ * SyncFolder worker. Performs the pending operations in the order they were requested.
+ *
+ * Created with the Looper of a new thread, started in
+ * {@link com.owncloud.android.services.OperationsService#onCreate()}.
+ */
+class SyncFolderHandler extends Handler {
+
+    private static final String TAG = SyncFolderHandler.class.getSimpleName();
+
+
+    OperationsService mService;
+
+    private IndexedForest<SynchronizeFolderOperation> mPendingOperations =
+            new IndexedForest<SynchronizeFolderOperation>();
+
+    private OwnCloudClient mOwnCloudClient = null;
+    private Account mCurrentAccount = null;
+    private FileDataStorageManager mStorageManager;
+    private SynchronizeFolderOperation mCurrentSyncOperation;
+
+
+    public SyncFolderHandler(Looper looper, OperationsService service) {
+        super(looper);
+        if (service == null) {
+            throw new IllegalArgumentException("Received invalid NULL in parameter 'service'");
+        }
+        mService = service;
+    }
+
+
+    /**
+     * Returns True when the folder located in 'remotePath' in the ownCloud account 'account', or any of its
+     * descendants, is being synchronized (or waiting for it).
+     *
+     * @param account       ownCloud account where the remote folder is stored.
+     * @param remotePath    The path to a folder that could be in the queue of synchronizations.
+     */
+    public boolean isSynchronizing(Account account, String remotePath) {
+        if (account == null || remotePath == null) return false;
+        return (mPendingOperations.contains(account, remotePath));
+    }
+
+
+    @Override
+    public void handleMessage(Message msg) {
+        Pair<Account, String> itemSyncKey = (Pair<Account, String>) msg.obj;
+        /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                    "Handling sync folder " + itemSyncKey.second);*/
+        doOperation(itemSyncKey.first, itemSyncKey.second);
+        mService.stopSelf(msg.arg1);
+    }
+
+
+    /**
+     * Performs the next operation in the queue
+     */
+    private void doOperation(Account account, String remotePath) {
+
+        /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                "Getting sync folder " + remotePath);*/
+        mCurrentSyncOperation = mPendingOperations.get(account, remotePath);
+
+        if (mCurrentSyncOperation != null) {
+            RemoteOperationResult result = null;
+
+            try {
+
+                if (mCurrentAccount == null || !mCurrentAccount.equals(account)) {
+                    mCurrentAccount = account;
+                    mStorageManager = new FileDataStorageManager(
+                            account,
+                            mService.getContentResolver()
+                    );
+                }   // else, reuse storage manager from previous operation
+
+                // always get client from client manager, to get fresh credentials in case of update
+                OwnCloudAccount ocAccount = new OwnCloudAccount(account, mService);
+                mOwnCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton().
+                        getClientFor(ocAccount, mService);
+
+                /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                        "Executing sync folder " + remotePath);*/
+                result = mCurrentSyncOperation.execute(mOwnCloudClient, mStorageManager);
+
+            } catch (AccountsException e) {
+                Log_OC.e(TAG, "Error while trying to get authorization", e);
+            } catch (IOException e) {
+                Log_OC.e(TAG, "Error while trying to get authorization", e);
+            } finally {
+                /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                        "Removing payload " + remotePath);*/
+
+                mPendingOperations.removePayload(account, remotePath);
+
+                mService.dispatchResultToOperationListeners(null, mCurrentSyncOperation, result);
+
+                sendBroadcastFinishedSyncFolder(account, remotePath, result.isSuccess());
+            }
+        }
+    }
+
+    public void add(Account account, String remotePath, SynchronizeFolderOperation syncFolderOperation){
+        mPendingOperations.putIfAbsent(account, remotePath, syncFolderOperation);
+        sendBroadcastNewSyncFolder(account, remotePath);    // TODO upgrade!
+    }
+
+
+    /**
+     * Cancels a pending or current sync' operation.
+     *
+     * @param account       ownCloud account where the remote file is stored.
+     * @param file          A file in the queue of pending synchronizations
+     */
+    public void cancel(Account account, OCFile file){
+        if (account == null || file == null) {
+            Log_OC.e(TAG, "Cannot cancel with NULL parameters");
+            return;
+        }
+        /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                "Removing sync folder " + file.getRemotePath());*/
+        Pair<SynchronizeFolderOperation, String> removeResult =
+                mPendingOperations.remove(account, file.getRemotePath());
+        SynchronizeFolderOperation synchronization = removeResult.first;
+        if (synchronization != null) {
+            /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                    "Canceling returned sync of " + file.getRemotePath());*/
+            synchronization.cancel();
+        } else {
+            // TODO synchronize?
+            if (mCurrentSyncOperation != null && mCurrentAccount != null &&
+                    mCurrentSyncOperation.getRemotePath().startsWith(file.getRemotePath()) &&
+                    account.name.equals(mCurrentAccount.name)) {
+                /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                        "Canceling current sync as descendant: " + mCurrentSyncOperation.getRemotePath());*/
+                mCurrentSyncOperation.cancel();
+            } else {
+                /*Log_OC.v(   "NOW " + TAG + ", thread " + Thread.currentThread().getName(),
+                        "Nothing else in cancelation of " + file.getRemotePath());*/
+            }
+        }
+
+        //sendBroadcastFinishedSyncFolder(account, file.getRemotePath());
+    }
+
+    /**
+     * TODO review this method when "folder synchronization" replaces "folder download"; this is a fast and ugly
+     * patch.
+     */
+    private void sendBroadcastNewSyncFolder(Account account, String remotePath) {
+        Intent added = new Intent(FileDownloader.getDownloadAddedMessage());
+        added.putExtra(FileDownloader.ACCOUNT_NAME, account.name);
+        added.putExtra(FileDownloader.EXTRA_REMOTE_PATH, remotePath);
+        added.putExtra(FileDownloader.EXTRA_FILE_PATH, FileStorageUtils.getSavePath(account.name) + remotePath);
+        mService.sendStickyBroadcast(added);
+    }
+
+    /**
+     * TODO review this method when "folder synchronization" replaces "folder download"; this is a fast and ugly
+     * patch.
+     */
+    private void sendBroadcastFinishedSyncFolder(Account account, String remotePath, boolean success) {
+        Intent finished = new Intent(FileDownloader.getDownloadFinishMessage());
+        finished.putExtra(FileDownloader.ACCOUNT_NAME, account.name);
+        finished.putExtra(FileDownloader.EXTRA_REMOTE_PATH, remotePath);
+        finished.putExtra(FileDownloader.EXTRA_FILE_PATH, FileStorageUtils.getSavePath(account.name) + remotePath);
+        finished.putExtra(FileDownloader.EXTRA_DOWNLOAD_RESULT, success);
+        mService.sendStickyBroadcast(finished);
+    }
+
+
+}

+ 2 - 2
src/com/owncloud/android/syncadapter/FileSyncAdapter.java

@@ -31,7 +31,7 @@ import com.owncloud.android.authentication.AuthenticatorActivity;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
-import com.owncloud.android.operations.SynchronizeFolderOperation;
+import com.owncloud.android.operations.RefreshFolderOperation;
 import com.owncloud.android.operations.UpdateOCVersionOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -260,7 +260,7 @@ public class FileSyncAdapter extends AbstractOwnCloudSyncAdapter {
         }
         */
         // folder synchronization
-        SynchronizeFolderOperation synchFolderOp = new SynchronizeFolderOperation(  folder, 
+        RefreshFolderOperation synchFolderOp = new RefreshFolderOperation(  folder,
                                                                                     mCurrentSyncTime, 
                                                                                     true,
                                                                                     mIsShareSupported,

+ 9 - 6
src/com/owncloud/android/ui/activity/ComponentsGetter.java

@@ -22,28 +22,31 @@ import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.files.FileOperationsHelper;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
+import com.owncloud.android.services.OperationsService.OperationsServiceBinder;
 
 public interface ComponentsGetter {
 
     /**
-     * Callback method invoked when the parent activity is fully created to get a reference to the FileDownloader service API.
-     * 
-     * @return  Directory to list firstly. Can be NULL.
+     * To be invoked when the parent activity is fully created to get a reference  to the FileDownloader service API.
      */
     public FileDownloaderBinder getFileDownloaderBinder();
 
     
     /**
-     * Callback method invoked when the parent activity is fully created to get a reference to the FileUploader service API.
-     * 
-     * @return  Directory to list firstly. Can be NULL.
+     * To be invoked when the parent activity is fully created to get a reference to the FileUploader service API.
      */
     public FileUploaderBinder getFileUploaderBinder();
 
     
+    /**
+     * To be invoked when the parent activity is fully created to get a reference to the OperationsSerivce service API.
+     */
+    public OperationsServiceBinder getOperationsServiceBinder();
+
     
     public FileDataStorageManager getStorageManager();
     
     public FileOperationsHelper getFileOperationsHelper();
 
+
 }

+ 13 - 2
src/com/owncloud/android/ui/activity/FileActivity.java

@@ -54,6 +54,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.operations.CreateShareOperation;
+import com.owncloud.android.operations.SynchronizeFolderOperation;
 import com.owncloud.android.operations.UnshareLinkOperation;
 
 import com.owncloud.android.services.OperationsService;
@@ -464,7 +465,10 @@ implements OnRemoteOperationListener, ComponentsGetter {
         } else if (operation instanceof UnshareLinkOperation) {
             onUnshareLinkOperationFinish((UnshareLinkOperation)operation, result);
         
-        } 
+        } else if (operation instanceof SynchronizeFolderOperation) {
+            onSynchronizeFolderOperationFinish((SynchronizeFolderOperation)operation, result);
+
+        }
     }
 
     protected void requestCredentialsUpdate() {
@@ -506,7 +510,14 @@ implements OnRemoteOperationListener, ComponentsGetter {
             t.show();
         } 
     }
-    
+
+    private void onSynchronizeFolderOperationFinish(SynchronizeFolderOperation operation, RemoteOperationResult result) {
+        if (!result.isSuccess() && result.getCode() != ResultCode.CANCELLED){
+            Toast t = Toast.makeText(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()),
+                    Toast.LENGTH_LONG);
+            t.show();
+        }
+    }
     
     protected void updateFileFromDB(){
         OCFile file = getFile();

+ 36 - 13
src/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -92,7 +92,7 @@ import com.owncloud.android.operations.MoveFileOperation;
 import com.owncloud.android.operations.RemoveFileOperation;
 import com.owncloud.android.operations.RenameFileOperation;
 import com.owncloud.android.operations.SynchronizeFileOperation;
-import com.owncloud.android.operations.SynchronizeFolderOperation;
+import com.owncloud.android.operations.RefreshFolderOperation;
 import com.owncloud.android.operations.UnshareLinkOperation;
 import com.owncloud.android.services.observer.FileObserverService;
 import com.owncloud.android.syncadapter.FileSyncAdapter;
@@ -810,8 +810,8 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener {
         IntentFilter syncIntentFilter = new IntentFilter(FileSyncAdapter.EVENT_FULL_SYNC_START);
         syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_END);
         syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED);
-        syncIntentFilter.addAction(SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED);
-        syncIntentFilter.addAction(SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED);
+        syncIntentFilter.addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED);
+        syncIntentFilter.addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED);
         mSyncBroadcastReceiver = new SyncBroadcastReceiver();
         registerReceiver(mSyncBroadcastReceiver, syncIntentFilter);
         //LocalBroadcastManager.getInstance(this).registerReceiver(mSyncBroadcastReceiver, syncIntentFilter);
@@ -1099,9 +1099,9 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener {
                             setFile(currentFile);
                         }
                         
-                        mSyncInProgress = (!FileSyncAdapter.EVENT_FULL_SYNC_END.equals(event) && !SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.equals(event));
+                        mSyncInProgress = (!FileSyncAdapter.EVENT_FULL_SYNC_END.equals(event) && !RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.equals(event));
                                 
-                        if (SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED.
+                        if (RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED.
                                     equals(event) &&
                                 /// TODO refactor and make common
                                 synchResult != null && !synchResult.isSuccess() &&  
@@ -1255,26 +1255,36 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener {
 
 
     /**
-     * Class waiting for broadcast events from the {@link FielDownloader} service.
+     * Class waiting for broadcast events from the {@link FileDownloader} service.
      * 
      * Updates the UI when a download is started or finished, provided that it is relevant for the
      * current folder.
      */
     private class DownloadFinishReceiver extends BroadcastReceiver {
+
+        //int refreshCounter = 0;
         @Override
         public void onReceive(Context context, Intent intent) {
             try {
                 boolean sameAccount = isSameAccount(context, intent);
                 String downloadedRemotePath = intent.getStringExtra(FileDownloader.EXTRA_REMOTE_PATH);
                 boolean isDescendant = isDescendant(downloadedRemotePath);
-    
+
                 if (sameAccount && isDescendant) {
-                    refreshListOfFilesFragment();
-                    refreshSecondFragment(intent.getAction(), downloadedRemotePath, intent.getBooleanExtra(FileDownloader.EXTRA_DOWNLOAD_RESULT, false));
+                    String linkedToRemotePath = intent.getStringExtra(FileDownloader.EXTRA_LINKED_TO_PATH);
+                    if (linkedToRemotePath == null || isAscendant(linkedToRemotePath)) {
+                        //Log_OC.v(TAG, "refresh #" + ++refreshCounter);
+                        refreshListOfFilesFragment();
+                    }
+                    refreshSecondFragment(
+                            intent.getAction(),
+                            downloadedRemotePath,
+                            intent.getBooleanExtra(FileDownloader.EXTRA_DOWNLOAD_RESULT, false)
+                    );
                 }
     
                 if (mWaitingToSend != null) {
-                    mWaitingToSend = getStorageManager().getFileByPath(mWaitingToSend.getRemotePath()); // Update the file to send
+                    mWaitingToSend = getStorageManager().getFileByPath(mWaitingToSend.getRemotePath());
                     if (mWaitingToSend.isDown()) { 
                         sendDownloadedFile();
                     }
@@ -1289,7 +1299,19 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener {
 
         private boolean isDescendant(String downloadedRemotePath) {
             OCFile currentDir = getCurrentDir();
-            return (currentDir != null && downloadedRemotePath != null && downloadedRemotePath.startsWith(currentDir.getRemotePath()));
+            return (
+                currentDir != null &&
+                downloadedRemotePath != null &&
+                downloadedRemotePath.startsWith(currentDir.getRemotePath())
+            );
+        }
+
+        private boolean isAscendant(String linkedToRemotePath) {
+            OCFile currentDir = getCurrentDir();
+            return (
+                currentDir != null &&
+                currentDir.getRemotePath().startsWith(linkedToRemotePath)
+            );
         }
 
         private boolean isSameAccount(Context context, Intent intent) {
@@ -1725,6 +1747,7 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener {
 
     private void requestForDownload() {
         Account account = getAccount();
+        //if (!mWaitingToPreview.isDownloading()) {
         if (!mDownloaderBinder.isDownloading(account, mWaitingToPreview)) {
             Intent i = new Intent(this, FileDownloader.class);
             i.putExtra(FileDownloader.EXTRA_ACCOUNT, account);
@@ -1753,7 +1776,7 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener {
         mSyncInProgress = true;
                 
         // perform folder synchronization
-        RemoteOperation synchFolderOp = new SynchronizeFolderOperation( folder,  
+        RemoteOperation synchFolderOp = new RefreshFolderOperation( folder,
                                                                         currentSyncTime, 
                                                                         false,
                                                                         getFileOperationsHelper().isSharedSupported(),
@@ -1782,7 +1805,7 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener {
     
     private void requestForDownload(OCFile file) {
         Account account = getAccount();
-        if (!mDownloaderBinder.isDownloading(account, file)) {
+        if (!mDownloaderBinder.isDownloading(account, mWaitingToPreview)) {
             Intent i = new Intent(this, FileDownloader.class);
             i.putExtra(FileDownloader.EXTRA_ACCOUNT, account);
             i.putExtra(FileDownloader.EXTRA_FILE, file);

+ 6 - 6
src/com/owncloud/android/ui/activity/FolderPickerActivity.java

@@ -55,7 +55,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.operations.CreateFolderOperation;
-import com.owncloud.android.operations.SynchronizeFolderOperation;
+import com.owncloud.android.operations.RefreshFolderOperation;
 import com.owncloud.android.syncadapter.FileSyncAdapter;
 import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
 import com.owncloud.android.ui.fragment.FileFragment;
@@ -208,7 +208,7 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C
         mSyncInProgress = true;
                 
         // perform folder synchronization
-        RemoteOperation synchFolderOp = new SynchronizeFolderOperation( folder,  
+        RemoteOperation synchFolderOp = new RefreshFolderOperation( folder,
                                                                         currentSyncTime, 
                                                                         false,
                                                                         getFileOperationsHelper().isSharedSupported(),
@@ -236,8 +236,8 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C
         IntentFilter syncIntentFilter = new IntentFilter(FileSyncAdapter.EVENT_FULL_SYNC_START);
         syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_END);
         syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED);
-        syncIntentFilter.addAction(SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED);
-        syncIntentFilter.addAction(SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED);
+        syncIntentFilter.addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED);
+        syncIntentFilter.addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED);
         mSyncBroadcastReceiver = new SyncBroadcastReceiver();
         registerReceiver(mSyncBroadcastReceiver, syncIntentFilter);
         
@@ -478,9 +478,9 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C
                         }
                         
                         mSyncInProgress = (!FileSyncAdapter.EVENT_FULL_SYNC_END.equals(event) && 
-                                !SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.equals(event));
+                                !RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.equals(event));
                                 
-                        if (SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED.
+                        if (RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED.
                                     equals(event) &&
                                 /// TODO refactor and make common
                                 synchResult != null && !synchResult.isSuccess() &&  

+ 55 - 1
src/com/owncloud/android/ui/activity/Preferences.java

@@ -79,8 +79,13 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa
     private String mAccountName;
     private boolean mShowContextMenu = false;
     private String mUploadPath;
+    private PreferenceCategory mPrefInstantUploadCategory;
+    private Preference mPrefInstantUpload;
     private Preference mPrefInstantUploadPath;
+    private Preference mPrefInstantUploadPathWiFi;
+    private Preference mPrefInstantVideoUpload;
     private Preference mPrefInstantVideoUploadPath;
+    private Preference mPrefInstantVideoUploadPathWiFi;
     private String mUploadVideoPath;
 
 
@@ -275,7 +280,23 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa
                     }
                 });
         }
-
+        
+        mPrefInstantUploadCategory = (PreferenceCategory) findPreference("instant_uploading_category");
+        
+        mPrefInstantUploadPathWiFi =  findPreference("instant_upload_on_wifi");
+        mPrefInstantUpload = findPreference("instant_uploading");
+        
+        toggleInstantPictureOptions(((CheckBoxPreference) mPrefInstantUpload).isChecked());
+        
+        mPrefInstantUpload.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+            
+            @Override
+            public boolean onPreferenceChange(Preference preference, Object newValue) {
+                toggleInstantPictureOptions((Boolean) newValue);
+                return true;
+            }
+        });
+       
         mPrefInstantVideoUploadPath =  findPreference("instant_video_upload_path");
         if (mPrefInstantVideoUploadPath != null){
 
@@ -292,6 +313,19 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa
                     }
                 });
         }
+        
+        mPrefInstantVideoUploadPathWiFi =  findPreference("instant_video_upload_on_wifi");
+        mPrefInstantVideoUpload = findPreference("instant_video_uploading");
+        toggleInstantVideoOptions(((CheckBoxPreference) mPrefInstantVideoUpload).isChecked());
+        
+        mPrefInstantVideoUpload.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+            
+            @Override
+            public boolean onPreferenceChange(Preference preference, Object newValue) {
+                toggleInstantVideoOptions((Boolean) newValue);
+                return true;
+            }
+        });
             
         /* About App */
        pAboutApp = (Preference) findPreference("about_app");
@@ -304,6 +338,26 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa
        loadInstantUploadVideoPath();
 
     }
+    
+    private void toggleInstantPictureOptions(Boolean value){
+        if (value){
+            mPrefInstantUploadCategory.addPreference(mPrefInstantUploadPathWiFi);
+            mPrefInstantUploadCategory.addPreference(mPrefInstantUploadPath);
+        } else {
+            mPrefInstantUploadCategory.removePreference(mPrefInstantUploadPathWiFi);
+            mPrefInstantUploadCategory.removePreference(mPrefInstantUploadPath);
+        }
+    }
+    
+    private void toggleInstantVideoOptions(Boolean value){
+        if (value){
+            mPrefInstantUploadCategory.addPreference(mPrefInstantVideoUploadPathWiFi);
+            mPrefInstantUploadCategory.addPreference(mPrefInstantVideoUploadPath);
+        } else {
+            mPrefInstantUploadCategory.removePreference(mPrefInstantVideoUploadPathWiFi);
+            mPrefInstantUploadCategory.removePreference(mPrefInstantVideoUploadPath);
+        }
+    }
 
     @Override
     protected void onPause() {

+ 11 - 106
src/com/owncloud/android/ui/adapter/FileListListAdapter.java

@@ -1,6 +1,6 @@
 /* ownCloud Android client application
  *   Copyright (C) 2011  Bartek Przybylski
- *   Copyright (C) 2012-2014 ownCloud Inc.
+ *   Copyright (C) 2012-2015 ownCloud Inc.
  *
  *   This program is free software: you can redistribute it and/or modify
  *   it under the terms of the GNU General Public License version 2,
@@ -19,25 +19,18 @@ package com.owncloud.android.ui.adapter;
 
 
 import java.io.File;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.Vector;
 
-import third_parties.daveKoeller.AlphanumComparator;
 import android.accounts.Account;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.media.ThumbnailUtils;
 import android.preference.PreferenceManager;
 import android.text.format.DateUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.BaseAdapter;
-import android.widget.Filter;
-import android.widget.Filterable;
 import android.widget.GridView;
 import android.widget.ImageView;
 import android.widget.ListAdapter;
@@ -50,6 +43,7 @@ import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
+import com.owncloud.android.services.OperationsService.OperationsServiceBinder;
 import com.owncloud.android.ui.activity.ComponentsGetter;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.FileStorageUtils;
@@ -75,17 +69,14 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
     private FileDataStorageManager mStorageManager;
     private Account mAccount;
     private ComponentsGetter mTransferServiceGetter;
+
     private enum ViewType {LIST_ITEM, GRID_IMAGE, GRID_ITEM };
-    private Integer mSortOrder;
-    public static final Integer SORT_NAME = 0;
-    public static final Integer SORT_DATE = 1;
-    public static final Integer SORT_SIZE = 2;
-    private Boolean mSortAscending;
+
     private SharedPreferences mAppPreferences;
     
     public FileListListAdapter(
             boolean justFolders, 
-            Context context, 
+            Context context,
             ComponentsGetter transferServiceGetter
             ) {
         
@@ -93,7 +84,7 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
         mContext = context;
         mAccount = AccountUtils.getCurrentOwnCloudAccount(mContext);
         mTransferServiceGetter = transferServiceGetter;
-        
+
         mAppPreferences = PreferenceManager
                 .getDefaultSharedPreferences(mContext);
         
@@ -182,6 +173,7 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
         if (file != null){
 
             ImageView fileIcon = (ImageView) view.findViewById(R.id.thumbnail);
+            fileIcon.setTag(file.getFileId());
             TextView fileName;
             String name;
 
@@ -237,7 +229,10 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
                     localStateView.bringToFront();
                     FileDownloaderBinder downloaderBinder = mTransferServiceGetter.getFileDownloaderBinder();
                     FileUploaderBinder uploaderBinder = mTransferServiceGetter.getFileUploaderBinder();
-                    if (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, file)) {
+                    boolean downloading = (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, file));
+                    OperationsServiceBinder opsBinder = mTransferServiceGetter.getOperationsServiceBinder();
+                    downloading |= (opsBinder != null && opsBinder.isSynchronizing(mAccount, file.getRemotePath()));
+                    if (downloading) {
                         localStateView.setImageResource(R.drawable.downloading_file_indicator);
                         localStateView.setVisibility(View.VISIBLE);
                     } else if (uploaderBinder != null && uploaderBinder.isUploading(mAccount, file)) {
@@ -442,96 +437,6 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter {
                 && file.getPermissions().contains(PERMISSION_SHARED_WITH_ME));
     }
 
-    /**
-     * Sorts list by Date
-     * @param sortAscending true: ascending, false: descending
-     */
-    private void sortByDate(boolean sortAscending){
-        final Integer val;
-        if (sortAscending){
-            val = 1;
-        } else {
-            val = -1;
-        }
-        
-        Collections.sort(mFiles, new Comparator<OCFile>() {
-            public int compare(OCFile o1, OCFile o2) {
-                if (o1.isFolder() && o2.isFolder()) {
-                    Long obj1 = o1.getModificationTimestamp();
-                    return val * obj1.compareTo(o2.getModificationTimestamp());
-                }
-                else if (o1.isFolder()) {
-                    return -1;
-                } else if (o2.isFolder()) {
-                    return 1;
-                } else if (o1.getModificationTimestamp() == 0 || o2.getModificationTimestamp() == 0){
-                    return 0;
-                } else {
-                    Long obj1 = o1.getModificationTimestamp();
-                    return val * obj1.compareTo(o2.getModificationTimestamp());
-                }
-            }
-        });
-    }
-
-    /**
-     * Sorts list by Size
-     * @param sortAscending true: ascending, false: descending
-     */
-    private void sortBySize(boolean sortAscending){
-        final Integer val;
-        if (sortAscending){
-            val = 1;
-        } else {
-            val = -1;
-        }
-        
-        Collections.sort(mFiles, new Comparator<OCFile>() {
-            public int compare(OCFile o1, OCFile o2) {
-                if (o1.isFolder() && o2.isFolder()) {
-                    Long obj1 = getFolderSize(new File(FileStorageUtils.getDefaultSavePathFor(mAccount.name, o1)));
-                    return val * obj1.compareTo(getFolderSize(new File(FileStorageUtils.getDefaultSavePathFor(mAccount.name, o2))));
-                }
-                else if (o1.isFolder()) {
-                    return -1;
-                } else if (o2.isFolder()) {
-                    return 1;
-                } else if (o1.getFileLength() == 0 || o2.getFileLength() == 0){
-                    return 0;
-                } else {
-                    Long obj1 = o1.getFileLength();
-                    return val * obj1.compareTo(o2.getFileLength());
-                }
-            }
-        });
-    }
-
-    /**
-     * Sorts list by Name
-     * @param sortAscending true: ascending, false: descending
-     */
-    private void sortByName(boolean sortAscending){
-        final Integer val;
-        if (sortAscending){
-            val = 1;
-        } else {
-            val = -1;
-        }
-
-        Collections.sort(mFiles, new Comparator<OCFile>() {
-            public int compare(OCFile o1, OCFile o2) {
-                if (o1.isFolder() && o2.isFolder()) {
-                    return val * o1.getRemotePath().toLowerCase().compareTo(o2.getRemotePath().toLowerCase());
-                } else if (o1.isFolder()) {
-                    return -1;
-                } else if (o2.isFolder()) {
-                    return 1;
-                }
-                return val * new AlphanumComparator().compare(o1, o2);
-            }
-        });
-    }
-
     public void setSortOrder(Integer order, boolean ascending) {
         SharedPreferences.Editor editor = mAppPreferences.edit();
         editor.putInt("sortOrder", order);

+ 5 - 1
src/com/owncloud/android/ui/fragment/FileDetailFragment.java

@@ -348,7 +348,10 @@ public class FileDetailFragment extends FileFragment implements OnClickListener
             // configure UI for depending upon local state of the file
             FileDownloaderBinder downloaderBinder = mContainerActivity.getFileDownloaderBinder();
             FileUploaderBinder uploaderBinder = mContainerActivity.getFileUploaderBinder();
-            if (transferring || (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, file)) || (uploaderBinder != null && uploaderBinder.isUploading(mAccount, file))) {
+            if (transferring ||
+                    (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, file)) ||
+                    (uploaderBinder != null && uploaderBinder.isUploading(mAccount, file))
+                    ) {
                 setButtonsForTransferring();
                 
             } else if (file.isDown()) {
@@ -449,6 +452,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener
             progressText.setVisibility(View.VISIBLE);
             FileDownloaderBinder downloaderBinder = mContainerActivity.getFileDownloaderBinder();
             FileUploaderBinder uploaderBinder = mContainerActivity.getFileUploaderBinder();
+            //if (getFile().isDownloading()) {
             if (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, getFile())) {
                 progressText.setText(R.string.downloader_download_in_progress_ticker);
             } else if (uploaderBinder != null && uploaderBinder.isUploading(mAccount, getFile())) {

+ 1 - 1
src/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -133,7 +133,7 @@ public class OCFileListFragment extends ExtendedListFragment {
         boolean justFolders = (args == null) ? false : args.getBoolean(ARG_JUST_FOLDERS, false); 
         mAdapter = new FileListListAdapter(
                 justFolders,
-                getSherlockActivity(), 
+                getSherlockActivity(),
                 mContainerActivity
                 );
         setListAdapter(mAdapter);

+ 4 - 3
src/com/owncloud/android/ui/preview/FileDownloadFragment.java

@@ -211,10 +211,11 @@ public class FileDownloadFragment extends FileFragment implements OnClickListene
      * @param   transferring    When true, the view must be updated assuming that the holded file is 
      *                          downloading, no matter what the downloaderBinder says.
      */
+    /*
     public void updateView(boolean transferring) {
         // configure UI for depending upon local state of the file
-        FileDownloaderBinder downloaderBinder = (mContainerActivity == null) ? null : mContainerActivity.getFileDownloaderBinder();
-        if (transferring || (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, getFile()))) {
+        // TODO remove
+        if (transferring || getFile().isDownloading()) {
             setButtonsForTransferring();
             
         } else if (getFile().isDown()) {
@@ -227,7 +228,7 @@ public class FileDownloadFragment extends FileFragment implements OnClickListene
         getView().invalidate();
         
     }
-
+    */
 
     /**
      * Enables or disables buttons for a file being downloaded

+ 1 - 1
src/com/owncloud/android/ui/preview/PreviewImageActivity.java

@@ -426,7 +426,7 @@ ViewPager.OnPageChangeListener, OnRemoteOperationListener {
     
 
     /**
-     * Class waiting for broadcast events from the {@link FielDownloader} service.
+     * Class waiting for broadcast events from the {@link FileDownloader} service.
      * 
      * Updates the UI when a download is started or finished, provided that it is relevant for the
      * folder displayed in the gallery.

+ 16 - 0
src/com/owncloud/android/utils/ErrorMessageAdapter.java

@@ -36,6 +36,7 @@ import com.owncloud.android.operations.MoveFileOperation;
 import com.owncloud.android.operations.RemoveFileOperation;
 import com.owncloud.android.operations.RenameFileOperation;
 import com.owncloud.android.operations.SynchronizeFileOperation;
+import com.owncloud.android.operations.SynchronizeFolderOperation;
 import com.owncloud.android.operations.UnshareLinkOperation;
 import com.owncloud.android.operations.UploadFileOperation;
 
@@ -206,6 +207,21 @@ public class ErrorMessageAdapter {
                 // Show a Message, operation finished without success
                 message = res.getString(R.string.move_file_error);
             }
+        } else if (operation instanceof SynchronizeFolderOperation) {
+
+            if (!result.isSuccess()) {
+                String folderPathName = new File(
+                        ((SynchronizeFolderOperation) operation).getFolderPath()).getName();
+                if (result.getCode() == ResultCode.FILE_NOT_FOUND) {
+                    message = String.format(res.getString(R.string.sync_current_folder_was_removed),
+                            folderPathName);
+
+                } else {    // Generic error
+                    // Show a Message, operation finished without success
+                    message = String.format(res.getString(R.string.download_folder_failed_content),
+                            folderPathName);
+                }
+            }
         }
         
         return message;

+ 20 - 4
src/com/owncloud/android/utils/FileStorageUtils.java

@@ -36,6 +36,7 @@ import android.preference.PreferenceManager;
 import android.net.Uri;
 import android.os.Environment;
 import android.os.StatFs;
+import android.webkit.MimeTypeMap;
 
 
 /**
@@ -135,7 +136,7 @@ public class FileStorageUtils {
     /**
      * Creates and populates a new {@link RemoteFile} object with the data read from an {@link OCFile}.
      * 
-     * @param oCFile    OCFile 
+     * @param ocFile    OCFile
      * @return          New RemoteFile instance representing the resource described by ocFile.
      */
     public static RemoteFile fillRemoteFile(OCFile ocFile){
@@ -171,7 +172,7 @@ public class FileStorageUtils {
     
     /**
      * Sorts list by Date
-     * @param sortAscending true: ascending, false: descending
+     * @param files
      */
     public static Vector<OCFile> sortByDate(Vector<OCFile> files){
         final Integer val;
@@ -239,7 +240,7 @@ public class FileStorageUtils {
 
     /**
      * Sorts list by Name
-     * @param sortAscending true: ascending, false: descending
+     * @param files     files to sort
      */
     public static Vector<OCFile> sortByName(Vector<OCFile> files){
         final Integer val;
@@ -284,6 +285,21 @@ public class FileStorageUtils {
             return result;
         }
         return 0;
-    } 
+    }
+
+    /**
+     * Mimetype String of a file
+     * @param path
+     * @return
+     */
+    public static String getMimeTypeFromName(String path) {
+        String extension = "";
+        int pos = path.lastIndexOf('.');
+        if (pos >= 0) {
+            extension = path.substring(pos + 1);
+        }
+        String result = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase());
+        return (result != null) ? result : "";
+    }
   
 }

+ 173 - 0
user_manual/Makefile

@@ -0,0 +1,173 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html       to make standalone HTML files"
+	@echo "  dirhtml    to make HTML files named index.html in directories"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  pickle     to make pickle files"
+	@echo "  json       to make JSON files"
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
+	@echo "  qthelp     to make HTML files and a qthelp project"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
+	@echo "  epub       to make an epub"
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+	@echo "  pdf        to make PDF files"
+	@echo "  text       to make text files"
+	@echo "  man        to make manual pages"
+	@echo "  texinfo    to make Texinfo files"
+	@echo "  info       to make Texinfo files and run them through makeinfo"
+	@echo "  gettext    to make PO message catalogs"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck  to check all external links for integrity"
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html:   html-org
+
+html-all: html-release html-org html-com
+
+html-release:
+	$(SPHINXBUILD) -b html -D html_theme='owncloud_release' $(ALLSPHINXOPTS) $(BUILDDIR)/html/release
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html/release."
+
+html-org:
+	$(SPHINXBUILD) -b html -D html_theme='owncloud_org' $(ALLSPHINXOPTS) $(BUILDDIR)/html/org
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html/org."
+
+html-com:
+	$(SPHINXBUILD) -b html -D html_theme='owncloud_com' $(ALLSPHINXOPTS) $(BUILDDIR)/html/com
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html/com."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+	@echo
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/OwncloudDocumentation.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/OwncloudDocumentation.qhc"
+
+devhelp:
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+	@echo
+	@echo "Build finished."
+	@echo "To view the help file:"
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/OwncloudDocumentation"
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/OwncloudDocumentation"
+	@echo "# devhelp"
+
+epub:
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
+	      "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through pdflatex..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+pdf:
+	$(SPHINXBUILD) -b pdf $(ALLSPHINXOPTS) $(BUILDDIR)/pdf
+	@echo
+	@echo "build finished. the text files are in $(BUILDDIR)/pdf."
+
+text:
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+	@echo
+	@echo "build finished. the text files are in $(BUILDDIR)/text."
+
+man:
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+	@echo
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo
+	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+	@echo "Run \`make' in that directory to run these through makeinfo" \
+	      "(use \`make info' here to do that automatically)."
+
+info:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo "Running Texinfo files through makeinfo..."
+	make -C $(BUILDDIR)/texinfo info
+	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+	@echo
+	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."

+ 115 - 0
user_manual/android_app.rst

@@ -0,0 +1,115 @@
+==============================
+Using the ownCloud Android App
+==============================
+
+Accessing your files on your ownCloud server via the Web interface is easy and 
+convenient, as you can use any Web browser on any operating system without 
+installing special client software. However, the ownCloud Android app offers 
+some advantages over the Web interface:
+
+* A simplified interface that fits nicely on a tablet or smartphone
+* Automatic synchronization of your files
+* Instant uploads of photos or videos recorded on your Android device
+* Easily add files from your device to ownCloud
+* Two-factor authentication
+
+Getting the ownCloud Android App
+--------------------------------
+
+One way to get your ownCloud Android app is to log into your ownCloud server 
+from your Android device using a Web browser such as Chrome, Firefox, or 
+Dolphin. The first time you log in to a new ownCloud account you'll see a screen 
+with a download link to the ownCloud app in the `Google Play store
+<https://play.google.com/store/apps/details?id=com.owncloud.android>`_.
+
+.. figure:: images/android-first-screen.jpg
+
+You will also find these links on your Personal page in the Web interface,
+
+You can also get it from the `Amazon App store 
+<http://www.amazon.com/ownCloud-Inc/dp/B00944PQMK/>`_, and get source code and 
+more information from the `ownCloud download page 
+<http://owncloud.org/install/#mobile>`_.
+
+Connecting to Your ownCloud Server
+----------------------------------
+
+The first time you run your ownCloud Android app it opens to a configuration 
+screen. Enter your server URL, login name, password, and click the Connect 
+button. (Click the eyeball to the right of your password to expose your 
+password.)
+
+.. figure:: images/android-new-account.png
+
+For best security your ownCloud server should be SSL-enabled, so that you can 
+connect via ``https``. The ownCloud app will test your connection as soon as 
+you enter it and tell you if you entered it correctly. If your server has a 
+self-signed SSL certificate you'll get a scary warning how it is not to be 
+trusted. Click the OK button to accept the certificate and complete your account 
+setup.
+
+.. figure:: images/android-ssl-cert.png
+
+Managing Files
+--------------
+
+Now you should see the Files page of your ownCloud account. Click the overflow 
+button at the top right (that's the one with three vertical dots, and that is 
+really what it is called) to open a user menu. ``Refresh account`` refreshes the 
+page view. ``Settings`` take you to your settings menu. ``Sort`` gives you the 
+option to sort your files by date, or alphabetically.
+
+.. figure:: images/android-files-page.png
+
+The little file folder icon to the left of the overflow button opens a dialog to 
+create a new folder. The arrow button opens a file upload dialog, and you can 
+either upload content from other Android apps such as Google Drive, the Gallery, 
+your music player, or from your Android filesystem. When you add a new file 
+you will see a confirmation on the top left when it has uploaded successfully, 
+and it is immediately synchronized with the server.
+
+.. figure:: images/android-upload.png
+
+All files (that you have permission to access) on your ownCloud server are 
+displayed in your Android app, but are not downloaded until you download them. 
+Downloaded files are marked with a green arrow.
+
+.. figure:: images/android-file-list.png
+
+Download and preview a file with a short press on the filename.  When the file 
+is in preview mode, a short press on the overflow button opens a menu with 
+options for sharing, opening with an app, removing, sending, and displaying file 
+details. 
+
+.. figure:: images/android-file.png
+
+
+A long press on the filename does not download it, but opens a dialog with 
+options for sharing, downloading, renaming, moving, removing, sending, and 
+viewing file details. 
+
+
+.. figure:: images/android-file-options.png
+
+
+Settings
+--------
+
+The Settings screen offers a number of useful options. In the Accounts 
+section you can configure multiple ownCloud accounts.
+
+The Security section sets up strong two-factor authentication by allowing you 
+to add a PIN (personal identification number) to access your account.  
+
+The Instant Uploads section creates a directory, :file:`/InstantUpload`, and 
+any photos or videos created with your Android device's camera are instantly 
+uploaded to this directory. You also have the option to choose any other 
+existing directory. Another nice option is Upload Pictures/Video via WiFi Only, 
+to conserve your Internet data usage.
+
+.. figure:: images/android-settings.png
+
+The bottom section of the Settings screen has links to help and the 
+app's version number.
+
+.. figure:: images/android-help.png

+ 293 - 0
user_manual/conf.py

@@ -0,0 +1,293 @@
+# -*- coding: utf-8 -*-
+#
+# ownCloud Documentation documentation build configuration file, created by
+# sphinx-quickstart on Mon Oct 22 23:16:40 2012.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os, inspect
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+#path to this script
+scriptpath = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.todo']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = [scriptpath+'/ocdoc/_shared_assets/templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'ownCloud Android App Manual'
+copyright = u'2013-2015, The ownCloud developers'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '1.6.2'
+# The full version, including alpha/beta/rc tags.
+release = version
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build','scripts/*', 'ocdoc/*']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+2
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+html_theme_path = [scriptpath+'/ocdoc/_shared_assets/themes']
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#html_theme = 'bootstrap'
+html_theme = 'default'
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+html_short_title = "Android App Manual"
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = [scriptpath+'/ocdoc/_shared_assets/static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+html_show_sphinx = False
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'ownCloudAndroidAppManual'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'ownCloudAndroidAppManual.tex', u'ownCloud Android App Manual',
+   u'The ownCloud developers', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    ('owncloud.1', 'owncloud', u'Android synchronisation and file management utility.',
+     [u'The ownCloud developers'], 1),
+    ('owncloudcmd.1', 'owncloudcmd', u'ownCloud Android app.',
+     [u'The ownCloud developers'], 1),
+]
+
+# If true, show URL addresses after external links.
+man_show_urls = True
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+  ('index', 'ownCloudClientManual', u'ownCloud Android App Manual',
+   u'The ownCloud developers', 'ownCloud', 'The ownCloud Android App Manual.',
+   'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+
+# -- Options for Epub output ---------------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = u'ownCloud Android App Manual'
+epub_author = u'The ownCloud developers'
+epub_publisher = u'The ownCloud developers'
+epub_copyright = u'2013-2015, The ownCloud developers'
+
+# The language of the text. It defaults to the language option
+# or en if the language is not set.
+#epub_language = ''
+
+# The scheme of the identifier. Typical schemes are ISBN or URL.
+#epub_scheme = ''
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+#epub_identifier = ''
+
+# A unique identification for the text.
+#epub_uid = ''
+
+# A tuple containing the cover image and cover page html template filenames.
+#epub_cover = ()
+
+# HTML files that should be inserted before the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+#epub_pre_files = []
+
+# HTML files shat should be inserted after the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+#epub_post_files = []
+
+# A list of files that should not be packed into the epub file.
+#epub_exclude_files = []
+
+# The depth of the table of contents in toc.ncx.
+#epub_tocdepth = 3
+
+# Allow duplicate toc entries.
+#epub_tocdup = True
+
+# Include todos?
+todo_include_todos = True

BIN
user_manual/images/android-downloads.png


BIN
user_manual/images/android-file-list.png


BIN
user_manual/images/android-file-options.png


BIN
user_manual/images/android-file.png


BIN
user_manual/images/android-files-page.png


BIN
user_manual/images/android-first-screen.jpg


BIN
user_manual/images/android-help.png


BIN
user_manual/images/android-new-account.png


BIN
user_manual/images/android-settings.png


BIN
user_manual/images/android-ssl-cert.png


BIN
user_manual/images/android-upload.png


+ 9 - 0
user_manual/index.rst

@@ -0,0 +1,9 @@
+.. _contents:
+
+ownCloud Android App Manual
+==============================
+
+.. toctree::
+   :maxdepth: 2
+ 
+   android_app

+ 199 - 0
user_manual/make.bat

@@ -0,0 +1,199 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+	set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+	:help
+	echo.Please use `make ^<target^>` where ^<target^> is one of
+	echo.  html       to make standalone HTML files
+	echo.  dirhtml    to make HTML files named index.html in directories
+	echo.  singlehtml to make a single large HTML file
+	echo.  pdf        to make a PDF file with rst2pdf
+	echo.  pickle     to make pickle files
+	echo.  json       to make JSON files
+	echo.  htmlhelp   to make HTML files and a HTML help project
+	echo.  qthelp     to make HTML files and a qthelp project
+	echo.  devhelp    to make HTML files and a Devhelp project
+	echo.  epub       to make an epub
+	echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+	echo.  text       to make text files
+	echo.  man        to make manual pages
+	echo.  texinfo    to make Texinfo files
+	echo.  gettext    to make PO message catalogs
+	echo.  changes    to make an overview over all changed/added/deprecated items
+	echo.  linkcheck  to check all external links for integrity
+	echo.  doctest    to run all doctests embedded in the documentation if enabled
+	goto end
+)
+
+if "%1" == "clean" (
+	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+	del /q /s %BUILDDIR%\*
+	goto end
+)
+
+if "%1" == "html" (
+	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+	goto end
+)
+
+if "%1" == "dirhtml" (
+	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+	goto end
+)
+
+if "%1" == "singlehtml" (
+	%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+	goto end
+)
+
+if "%1" == "pdf" (
+	%SPHINXBUILD% -b pdf %ALLSPHINXOPTS% %BUILDDIR%/pdf
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The PDF file is in %BUILDDIR%/pdf.
+	goto end
+)
+
+if "%1" == "pickle" (
+	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can process the pickle files.
+	goto end
+)
+
+if "%1" == "json" (
+	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can process the JSON files.
+	goto end
+)
+
+if "%1" == "htmlhelp" (
+	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+	goto end
+)
+
+if "%1" == "qthelp" (
+	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\OwncloudDocumentation.qhcp
+	echo.To view the help file:
+	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\OwncloudDocumentation.ghc
+	goto end
+)
+
+if "%1" == "devhelp" (
+	%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished.
+	goto end
+)
+
+if "%1" == "epub" (
+	%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The epub file is in %BUILDDIR%/epub.
+	goto end
+)
+
+if "%1" == "latex" (
+	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+	goto end
+)
+
+if "%1" == "text" (
+	%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The text files are in %BUILDDIR%/text.
+	goto end
+)
+
+if "%1" == "man" (
+	%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The manual pages are in %BUILDDIR%/man.
+	goto end
+)
+
+if "%1" == "texinfo" (
+	%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+	goto end
+)
+
+if "%1" == "gettext" (
+	%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+	goto end
+)
+
+if "%1" == "changes" (
+	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.The overview file is in %BUILDDIR%/changes.
+	goto end
+)
+
+if "%1" == "linkcheck" (
+	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+	goto end
+)
+
+if "%1" == "doctest" (
+	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+	goto end
+)
+
+:end

+ 1 - 0
user_manual/ocdoc

@@ -0,0 +1 @@
+Subproject commit 343496c792616459e8204b6614fd42a1b16a6d68