From: masensio Date: Tue, 10 Mar 2015 13:58:21 +0000 (+0100) Subject: Merge branch 'develop' into accessibility X-Git-Tag: oc-android-1.7.1_signed^2~17^2 X-Git-Url: http://git.linex4red.de/pub/Android/ownCloud.git/commitdiff_plain/c963eb0fad38ec71f511de231bd81deadecc70fb?hp=21c775585804213116e490395df6b76561546d4e Merge branch 'develop' into accessibility Conflicts: src/com/owncloud/android/ui/adapter/FileListListAdapter.java --- diff --git a/.gitmodules b/.gitmodules index fa52fcd6..38f0f582 100644 --- a/.gitmodules +++ b/.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 diff --git a/AndroidManifest.xml b/AndroidManifest.xml index fd92d3d6..f0d1e9dc 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3,7 +3,7 @@ ownCloud Android client application Copyright (C) 2012 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, @@ -18,8 +18,8 @@ along with this program. If not, see . --> + android:versionCode="10700000" + android:versionName="1.7.0" xmlns:android="http://schemas.android.com/apk/res/android"> @@ -86,9 +86,6 @@ android:name=".ui.activity.Preferences" android:theme="@style/Theme.ownCloud" > - - - diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..51d52429 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +## 1.7.0 (19 February 2015) + +- Download full folders +- Grid view for images +- Remote thumbnails (OC Server 8.0+) +- Added number of files and folders at the end of the list +- "Open with" in contextual menu +- Downloads added to Media Provider +- Uploads: + + Local thumbnails in section "Files" + + Multiple selection in "Content from other apps" (Android 4.3+) +- Gallery: + + proper handling of EXIF + + obey sorting in the list of files +- Settings view updated +- Improved subjects in e-mails +- Bugs fixed +... + + diff --git a/build.gradle b/build.gradle index 46308aca..57ec86e5 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.14.0' + classpath 'com.android.tools.build:gradle:1.0.0' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8d63f892..5a1c4383 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Oct 15 10:45:44 CEST 2014 +#Sun Jan 18 17:01:43 CET 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip diff --git a/oc_jb_workaround/AndroidManifest.xml b/oc_jb_workaround/AndroidManifest.xml index 478e3c42..18ffb4c0 100644 --- a/oc_jb_workaround/AndroidManifest.xml +++ b/oc_jb_workaround/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="0100021" + android:versionName="1.0.21" > + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/grid_item.xml b/res/layout/grid_item.xml new file mode 100644 index 00000000..d0f3d0f1 --- /dev/null +++ b/res/layout/grid_item.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/list_fragment.xml b/res/layout/list_fragment.xml index 160edc1d..81b52100 100644 --- a/res/layout/list_fragment.xml +++ b/res/layout/list_fragment.xml @@ -1,9 +1,9 @@ - +--> + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_weight="1" > + android:footerDividersEnabled="false" + android:visibility="visible" > - + android:layout_height="match_parent" + android:visibility="visible" /> + - - - - - - - + android:layout_height="match_parent" + android:columnWidth="100dp" + android:gravity="center" + android:horizontalSpacing="2dp" + android:stretchMode="columnWidth" + android:verticalSpacing="2dp" + android:visibility="visible" /> + + + + + + + - + + \ No newline at end of file diff --git a/res/layout/list_item.xml b/res/layout/list_item.xml index c6c7b92f..c66ff730 100644 --- a/res/layout/list_item.xml +++ b/res/layout/list_item.xml @@ -3,7 +3,7 @@ ownCloud Android client application Copyright (C) 2012 Bartek Przybylski - Copyright (C) 2012-2013 ownCloud Inc. + 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, @@ -19,129 +19,141 @@ --> - - - - - - - - - - - + android:orientation="horizontal"> + + + + + + + + + + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:gravity="center_vertical" + android:orientation="vertical" > + android:textColor="#303030" + android:textSize="16dip" /> - + android:layout_marginLeft="4dp" + android:layout_marginRight="4dp" + android:weightSum="1"> + + + + + + - + - - - - - + + + + + + - + diff --git a/res/layout/loading_dialog.xml b/res/layout/loading_dialog.xml index 629d8e27..0dbb9ef3 100644 --- a/res/layout/loading_dialog.xml +++ b/res/layout/loading_dialog.xml @@ -1,4 +1,21 @@ + + + diff --git a/res/menu/account_picker_long_click.xml b/res/menu/account_picker_long_click.xml index b7e2dd37..df83e378 100644 --- a/res/menu/account_picker_long_click.xml +++ b/res/menu/account_picker_long_click.xml @@ -3,7 +3,7 @@ ownCloud Android client application Copyright (C) 2012 Bartek Przybylski - Copyright (C) 2012-2013 ownCloud Inc. + 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, diff --git a/res/menu/file_actions_menu.xml b/res/menu/file_actions_menu.xml index b60d542c..545f8ea6 100644 --- a/res/menu/file_actions_menu.xml +++ b/res/menu/file_actions_menu.xml @@ -3,7 +3,7 @@ ownCloud Android client application Copyright (C) 2012 Bartek Przybylski - Copyright (C) 2012-2013 ownCloud Inc. + 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, diff --git a/res/menu/main_menu.xml b/res/menu/main_menu.xml index 07056010..d35eba01 100644 --- a/res/menu/main_menu.xml +++ b/res/menu/main_menu.xml @@ -3,7 +3,7 @@ ownCloud Android client application Copyright (C) 2012 Bartek Przybylski - Copyright (C) 2012-2013 ownCloud Inc. + 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, diff --git a/res/raw-de/changelog.html b/res/raw-de/changelog.html index a54a6643..d6511201 100644 --- a/res/raw-de/changelog.html +++ b/res/raw-de/changelog.html @@ -2,7 +2,7 @@

- Dieses Gerät läuft mit Android 4.1.x. + Dieses Ger�t l�uft mit Android 4.1.x.

- In dieser Version von Android existiert ein Bug, der nach jedem Neustart eine erneute Eingabe der ownCloud Login-Informationen nötig macht. Um das zu umgehen installieren Sie bitte diese kostenlose Hilfs-App: + In dieser Version von Android existiert ein Bug, der nach jedem Neustart eine erneute Eingabe der ownCloud Login-Informationen n�tig macht. Um das zu umgehen installieren Sie bitte diese kostenlose Hilfs-App:

ownCloud Jelly Bean Workaround diff --git a/res/raw-es/changelog.html b/res/raw-es/changelog.html index 9321d520..e8f0e77f 100644 --- a/res/raw-es/changelog.html +++ b/res/raw-es/changelog.html @@ -2,7 +2,7 @@ Ümumi @@ -32,6 +38,8 @@ Dostuna məsləhət gör Geriyə cavab İşarələmək + Paylaşma ünvanını yadda saxla + Son paylaşılmış yüklənmə ünvanını yadda saxla %1$s-i ağıllı telefonunuzda yoxlayın! Mən sizi öz smartfonunuzda %1$s istifadə etmək üçün dəvət etmək istəyirəm! Burdan endirin: %2$s Serveri yoxla @@ -51,9 +59,14 @@ Heç bir kontent gəlmədi. Yukləmək üçün heçnə yoxdur. %1$s yayımlanmış kontent üçün yetkili deyil Yüklənmə gedir + saniyələr öncə Burda heçnə yoxdur. Nese yükləyin! Yüklənir... Bu qovluqda heç bir fayl movcud deyil. + qovluq + qovluqlar + fayl + fayllar Faylın üstünə sıxın ki, əlavə məlumat ekrana çıxsın. Həcm: Tip: @@ -177,6 +190,7 @@ Aşağıda göstərilən %5$s-də olan daxili və xarici fayl(lar) link edilmiş %1$s çoxlu hesab dəstəkləmir Sizin server düzgün istifadəçi id-si qaytarmır, xahiş olunur inzibatçı ilə əlaqə saxlayasınız Bu serverdə yenidən qeydiyyatdan keçmək olmur + Hesab göstərilən avadanlıqda mövcud deyil Faylı gündəmdə saxla Adı dəyiş Sil @@ -206,18 +220,88 @@ Aşağıda göstərilən %5$s-də olan daxili və xarici fayl(lar) link edilmiş Server sertifikati inamlı deyil - Server sertifikatının vaxtı bitmişdir - Server sertifikatının düzgün tarixi gələcəkdədir + URL sertifikatda olan host adına uyğun deyil + İstənilən halda bu sertifikata inanmaq istəyirsinizmi? + Sertifikat saxlanıla bilməz Detallar + Gizlə + Verilir: + Tərəfindən verilib: + Ümumi ad: + Təşkilat: + Alt təşkilatOrganizational unit: + Ölkə: + Dövlət: + Ərazi: + Etibarlılıq: + Kimdən: + Kimə: + İmza: + Alqıritm: + Sertifikat görünə bilməz. + - Səhv haqqında məlumat yoxdur + Bu bir yer doldurucusudur + yerdoldurucusu.txt + PNG Şəkil + 389 KB + 2012/05/18 12:23 + 12:23:45 + Şəkilləri yalnız WiFi üzərindən yüklə + Videoları yalnız WiFi üzərindən yüklə + /CəldYükləmə + Yüklənmə konflikti + Uzaq fayl %s local faylla sinxronizasiya edilmədi. Faylın kontentinin serverdə dəyişdirilməsinə davam edirik. + Birlikdə saxla + Sil yenidən yaz + Yükləmə + Şəkili göstər + Bu şəkil göstərilə bilməz + %1$s nüsxələnə bilməz %2$s local qovluğa + Yüklənmə ünvanı + Üzr istəyirik, sizin yerverdə paylaşıma izin verilmir. Xahiş olunur +inzibatçınızla əlaqə saxlayasınız. + Paylaşa bilinmir. + Bu faylın yada qovluğun paylaşımı zamanı səhv baş verdi + Paylaşımı dayandırmaq olmur. Xahiş olunur fayl mövcudluğunu yoxlayasınız Bu fayl və ya qovluğun yayımlanmasının dayandırılmasında səhv baş verdi Göndər linki nüsxələ Mübadilə buferinə nüsxələndi + Kritik səhv: əməliyyat yerinə yetirilə bilinmir + Serverlə əlaqəyə girdikdə səhv baş verdi. + Serveri gözlədiyimiz müddətdə səhv baş verdi, əməliyyat bitə bilməz + Serveri gözlədiyimiz müddətdə səhv baş verdi, əməliyyat bitə bilməz + Əməliyyat bitə bilməz, serverə çatmaq mümkün deyil Sizin yetkiniz yoxdur %s + faylın adını dəyişmək bu faylı silmək üçün bu faylı yayımlamaq üçün + fayl paylaşımını dayandırmaq fayl yaratmaq üçün bu qovluğa yükləmək üçün + Bu fayla serverdə artıq uzun müddətdir ki, çatmaq mümkün deyil Hesablar Hesab əlavə et + Təhlükəsiz qoşulma, təhlükəsiz olmayan istiqamətə yönlədirilmişdir + Jurnallar + Tarixçəni göndər + Jurnalların ötürülməsi üçün proqram təminatı tapılmadı! + %1$s Android proqram jurnalları + Data yüklənir... + Qeydiyyat tələb edilir Yalnış şifrə + Köçürmək + Burda heçnə yoxdur. Siz qovluq əlavə edə bilərsiniz! + Seç + Köçürmə mümkün olmur. Xahiş olunur faylın mövcudluğunu yoxlayasınız. + Qovluğu bu nəsilə köçürmək mümkün deyil + Fayl artıq mənsəb qovluğunda mövcuddur + Fayl və ya qovluğun köçürülməsi müddətində səhv baş verdi + bu faylı köçürtmək + Anında yükləmələr + Təhlükəsizlik + Video ünvanını yüklə + Qovluğun endirilməsinin %1$s hissəsi tamamlana bilməz + %1$s paylaşdı \"%2$s\" sizinlə diff --git a/res/values-bg-rBG/strings.xml b/res/values-bg-rBG/strings.xml index 27178f21..2cd1c818 100644 --- a/res/values-bg-rBG/strings.xml +++ b/res/values-bg-rBG/strings.xml @@ -282,6 +282,9 @@ Сигурна връзка е пренасочена по несигурен път. Доклади Изпрати История + Не са намерени журнали за изпращане от приложението. Инсталирайте приложението за електронна поща! + %1$s Android журнали на приложенията + Зареждане на данни... Нужна е идентификация Грешна парола Премести @@ -294,4 +297,6 @@ за да преместиш този файл Незабавно качване Сигурност + Качване на видео път + Свалянето на директорията %1$s не може да бъде завършено diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml index 4c2f8692..9ab386ca 100644 --- a/res/values-bs/strings.xml +++ b/res/values-bs/strings.xml @@ -1,7 +1,34 @@ + Učitaj + Datoteke Nova fascikla + Postavke + Pošalji + Više + Pomoć + Korisničko ime + Lozinka + Datoteke + Učitaj + Preuzmite + Podijelite vezu + Da + Ne + Ok + Prekini učitavanje + Odustani + Greška + Nepoznata greška + Promijeni lozinku + Kreiraj račun + Preimenuj + Pošalji + Potrebna autentifikacija + Pogrešna lozinka + Izaberite + Sigurnost diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index 9980cf2c..d0cd67a4 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -11,6 +11,12 @@ Configuració Detalls Envia + Ordena + Ordena per + + A-Z + Més nou - Més antic + General diff --git a/res/values-cs-rCZ/strings.xml b/res/values-cs-rCZ/strings.xml index 56b77025..897be007 100644 --- a/res/values-cs-rCZ/strings.xml +++ b/res/values-cs-rCZ/strings.xml @@ -40,7 +40,7 @@ Imprint Zapamatovat umístění sdílení Zapamatovat poslední umístění pro nahrání sdílených souborů - Zkuste %1$s na vašem smartphonu! + Zkuste %1$s na svém chytrém telefonu! Chtěl bych vás pozvat k používání %1$s na vašem chytrém telefonu!\nKe stažení zde: %2$s Zkontrolovat server Adresa serveru https://... @@ -178,14 +178,15 @@ Neúspěšné přihlášení Přístup zamítnut autorizačním serverem Neočekávaný stav; prosím vložte znovu URL adresu serveru - Vaše přihlášení vypršelo. Přihlašte se, prosím, znovu + Vaše přihlášení vypršelo. Přihlaste se prosím znovu Zadejte prosím aktuální heslo - Vaše přihlášení vypršelo. Přihlašte se, prosím, znovu + Vaše přihlášení vypršelo. Přihlaste se prosím znovu Připojuji se k přihlašovacímu serveru... Server nepodporuje tuto přihlašovací metodu %1$s nepodporuje více účtů Váš server nevrací správné přihlašovací ID, kontaktujte prosím svého správce systému Není možné provést ověření + V zařízení není zatím nastaven účet Udržovat soubor aktuální Přejmenovat Odstranit @@ -281,7 +282,7 @@ správce systému. Bezpečné spojení je přesměrováno na nezabezpečenou trasu. Logy Odeslat historii - Nebyla nalezena žádná aplikace pro zasílání logů. Nainstalujte poštovní aplikaci! + Nebyla nalezena žádná aplikace pro odesílání logů. Nainstalujte poštovní aplikaci! %1$s logy aplikace pro Android Načítání dat… Vyžadováno přihlášení @@ -297,4 +298,6 @@ správce systému. Okamžitá odesílání Zabezpečení Cesta pro nahrávání videí + Stažení adresáře %1$s nemohlo být dokončeno + %1$s sdílí \"%2$s\" s vámi diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml index 149862b0..337ea373 100644 --- a/res/values-da/strings.xml +++ b/res/values-da/strings.xml @@ -186,6 +186,7 @@ %1$s understøtter ikke multiple konti Din server retunere ikke et korrekt bruger-id. Kontakt venligst din administrator Kan ikke autentificere mod denne server + Kontoen findes endnu ikke på enheden Hold filen opdateret Omdøb Fjern @@ -296,4 +297,6 @@ Øjeblikkelige uploads Sikkerhed Sti til videoupload + Download af %1$s mappe kunne ikke fuldføres + %1$s delt \"%2$s\" med dig diff --git a/res/values-de-rDE/strings.xml b/res/values-de-rDE/strings.xml index b1a9e68a..99125374 100644 --- a/res/values-de-rDE/strings.xml +++ b/res/values-de-rDE/strings.xml @@ -160,7 +160,7 @@ Keine Netzwerkverbindung Sichere Verbindung nicht verfügbar. Verbindung hergestellt - Verbindungstest … + Verbindungstest… Fehlerhafte Server Konfiguration Ein Benutzerkonto für den gleichen Benutzer und Server existiert auf diesem Gerät bereits Der eingegebene Benutzer passt nicht zu dem Benutzer dieses Benutzerkontos @@ -187,6 +187,7 @@ Ihr Server gibt keine richtige Benutzerkennung zurück, bitte kontaktieren Sie einen Administrator ⇥ Die Legitimierung gegenüber dem Server konnte nicht durchgeführt werden + Das Benutzerkonto ist bis jetzt noch nicht auf dem Gerät vorhanden Datei aktuell halten Umbenennen Löschen @@ -298,4 +299,6 @@ Sofortiges Hochladen Sicherheit Verzeichnis zum Hochladen der Videos + Herunterladen des %1$s - Ordners konnte nicht abgeschlossen werden + %1$s hat „%2$s“ mit Ihnen geteilt diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 23cdb642..aa0fdb96 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -43,7 +43,7 @@ Probiere %1$s auf Deinem Smartphone! Ich möchte Dich zum Benutzen von %1$s auf Deinem Smartphone einladen!\nLade es hier herunter: %2$s Überprüfe den Server - Server-Adresse https://… + Serveradresse https://… Benutzername Passwort Ist %1$s neu für dich? @@ -61,7 +61,7 @@ Lade hoch Gerade eben Alles leer. Lade etwas hoch! - Ladevorgang … + Laden… Es befinden sich keine Dateien in diesem Ordner. Ordner Ordner @@ -75,7 +75,7 @@ Herunterladen Datei aktualisieren Datei wurde wärend des Uploads zu %1$s umbenannt - Link Teilen + Link teilen Link nicht mehr freigeben Ja Nein @@ -85,22 +85,22 @@ Abbrechen Speichern & schließen Fehler - Lädt ... + Lade… Unbekannter Fehler Über Passwort ändern Account löschen Account erstellen - Dateien hochladen von... + Dateien hochladen von… Ordnername - Hochladen... + Hochladen… %1$d%% Hochladen %2$s Hochladen erfolgreich %1$s wurde(n) erfolgreich hochgeladen Hochladen fehlgeschlagen Hochladen von %1$s konnte nicht abgeschlossen werden Hochladen fehlgeschlagen, Du musst dich nochmals anmelden - Herunterladen... + Herunterladen… %1$d%% Herunterladen %2$s Herunterladen erfolgreich %1$s wurde erfolgreich heruntergeladen @@ -155,12 +155,12 @@ Zurückspielen Knopf Play-/Pause Knopf Vorspulen Knopf - Autorisierung empfangen... - Anmeldungsversuch... + Autorisierung empfangen… + Anmeldeversuch… Keine Netzwerkverbindung Sichere Verbindung nicht verfügbar. Verbindung hergestellt - Verbindung testen... + Verbindung testen… Fehlerhafte Server Konfiguration Ein Benutzerkonto für den gleichen Benutzer und Server existiert auf diesem Gerät bereits Der eingegebene Benutzer passt nicht zu dem Benutzer dieses Benutzerkontos @@ -187,6 +187,7 @@ Dein Server gibt keine korrekte Benutzer-ID zurück, bitte kontaktiere einen Administrator Die Authentifizierung gegenüber dem Server konnte nicht durchgeführt werden + Das Benutzerkonto ist bis jetzt noch nicht auf dem Gerät vorhanden Datei aktuell halten Umbenennen Löschen @@ -209,7 +210,7 @@ Bitte warte einen Moment. Ein unerwartetes Problem ist aufgetreten. Bitte versuche, die Datei in einer anderen App zu öffnen Es wurde keine Datei ausgewählt. - Link senden an ... + Link senden an… Anmelden mit oAuth2 Verbinde mit dem oAuth2-Server. Die Identität der Website konnte nicht überprüft werden @@ -298,4 +299,6 @@ Sofortiges Hochladen Sicherheit Verzeichnis zum Hochladen der Videos + Herunterladen des %1$s - Ordners konnte nicht abgeschlossen werden + %1$s hat „%2$s“ mit Dir geteilt diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index b037f019..29f78e96 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -11,8 +11,10 @@ Ρυθμίσεις Λεπτομέρειες Αποστολή + Ταξινόμηση + Ταξινόμηση κατά - A-Z + A-Ω Νεότερο - Παλαιότερο General @@ -32,7 +38,10 @@ Recomendar a un amigo Sugerencias Imprint + Recordar compartir ubicación + Recordar la ultima ubicación compartida de subida ¡Intento %1$s en tu teléfono inteligente! + Quiero invitarte a usar %1$s en tu teléfono inteligente!\nDescárgalo aquí: %2$s Verificar Servidor Dirección del servidor https://... Nombre de usuario @@ -241,12 +250,50 @@ No subir Previsualización de imagen Esta imagen no puede ser mostrada + %1$s no pudo ser copiado a la carpeta local %2$s + Dirección de subida + Lo sentimos, compartir no esta activado en su servidor. Por favor contacte a su +⇥⇥administrator. + Imposible compartir. Por favor revise si el archivo existe + Un error ocurrió cuando se intentaba compartir el archivo o carpeta + Imposible dejar de compartir. Por favor revise si los archivos existen + Un error ocurrió cuando se intentaba dejar de compartir el archivo o carpeta Mandar + Copiar dirección url Copiado al portapapeles + Error critico: no se puede realizar operaciones + Un error ocurrió mientras se conectaba con el Servidor. + Un error ocurrió mientras se conectaba con el Servidor. La operación no se realizó + Un error ocurrió esperando al Servidor, la operación no se realizó + Operación no completada, Servidor no disponible. + Tu no tienes permiso %s + para renombrar este archivo + para borrar este archivo + para compartir este archivo + para dejar de compartir este archivo + para crear el archivo + para subir en esta carpeta + El archivo no esta mas disponible en este Servidor Cuentas + Añadir cuenta + Conexión segura redireccionada a una ruta insegura. + Registro + Enviar Historial + Aplicación para enviar registros no encontrada. Instale una aplicación de correo! + %1$s Registros de la aplicación Android + Cargando datos... Autentificación requerida Clave incorrecta + Mover + Nada aquí. Puedes agregar una carpeta! Elegir + Imposible mover. Por favor revisa si el archivo existe + El archivo ya existe en la carpeta destino + Un error ocurrió intentando mover el archivo o carpeta + para mover este archivo + Subida Instantánea Seguridad + Dirección de subida del video + La descarga de la carpeta %1$s no pudo ser completada diff --git a/res/values-es-rCR/strings.xml b/res/values-es-rCR/strings.xml index 69623e19..26dde5f1 100644 --- a/res/values-es-rCR/strings.xml +++ b/res/values-es-rCR/strings.xml @@ -1,6 +1,8 @@ + Archivos + Archivos diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 793c094b..e5faa345 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -7,7 +7,7 @@ Contenido de otras aplicaciones Archivos Abrir con - Nueva Carpeta + Nueva carpeta Configuración Detalles Enviar @@ -40,19 +40,19 @@ pie de imprenta Recordar la ubicación de los archivos compartidos Recordar la ubicación de los últimos archivos compartidos subidos - Prueba %1$s en tu smarthphone! + ¡Prueba %1$s en su smarthphone! ¡Quiero invitarle a usar %1$s en su smartphone!\nDescárguelo aquí: %2$s Compruebe el servidor. Dirección del servidor https://… Nombre de usuario Contraseña - Nuevo para %1$s? + ¿Nuevo en %1$s? Archivos Conectar Subir Escoger carpeta de carga: No se encontró la cuenta - No hay cuentas de %1$s en tu dispositivo. Por favor configura una cuenta primero. + No hay cuentas de %1$s en su dispositivo. Por favor, configure una cuenta primero. Configuración Salir No hay contenido para subir @@ -85,7 +85,7 @@ Cancelar Guardar & Salir Error - Cargando ... + Cargando... Error desconocido Acerca de Cambiar contraseña @@ -100,7 +100,7 @@ Error en la subida La subida de %1$s no se pudo completar La carga falló, necesita volver a iniciar sesión - Descargando ... + Descargando... %1$d%% Descargado de %2$s Descarga completa %1$s se ha descargado con éxito @@ -108,7 +108,7 @@ La descarga de %1$s no se pudo completar No descargado Descarga fallida, necesita reinicar la sesión - Elige una cuenta + Elija una cuenta Falló la sincronización La sincronización falló, debe reiniciar la sesión La sincronización de %1$s s no se pudo completar @@ -127,7 +127,7 @@ Local: %1$s Remoto: %1$s No hay suficiente espacio para copiar los archivos seleccionados a la carpeta %1$s. ¿Desea moverlos en vez de copiarlos? - Por favor, inserta tu PIN de aplicación + Por favor, inserte su PIN de aplicación Introduzca un PIN para la aplicación Se solicitará el PIN cada vez que se inicie la aplicación Repita el PIN para la aplicación, por favor @@ -142,8 +142,8 @@ %1$s reproducción finalizada No se encontró el archivo multimedia No se ha proporcionado cuenta - El archivo no esta en una cuenta valida - Codec No Soportado + El archivo no está en una cuenta válida + Codec no soportado El archivo de medios no pudo ser leído Archivo no codificado correctamente Tiempo de espera agotado en el intento de reproducción @@ -151,8 +151,8 @@ El archivo de medios no se puede reproducir con el reproductor de medios por defecto Error de seguridad al intentar reproducir %1$s Error de entrada al intentar reproducir %1$s - Error inesperado intentando reproducir %1$s - Botón Rebobinado + Error inesperado al intentar reproducir %1$s + Botón de rebobinado Botón de reproducción o pausa Botón avance rápido Consiguiendo autorización... @@ -174,7 +174,7 @@ No se reconoce la versión del servidor No se ha podido establecer la conexión Conexión segura establecida - Nombre de usuario o contraseña incorrecta + Nombre de usuario o contraseña incorrectos Autorización no satisfactoria Acceso denegado por servidor de autorización Estado inesperado; por favor, introduzca la URL del servidor de nuevo @@ -187,6 +187,7 @@ Su servidor no está retornando una identificación de usuario correcta; contacte a un administrador No puede autenticarse en este servidor. + Aún no existe la cuenta en el dispositivo Mantener el archivo actualizado Renombrar Borrar @@ -207,7 +208,7 @@ Carácteres ilegales: / \\ < > : \" | ? * El nombre de archivo no puede estar vacío Espere un momento - Problema inesperado; por favor, prueba otra app para seleccionar el archivo + Problema inesperado; por favor, pruebe otra app para seleccionar el archivo No hay ficheros seleccionados. Enviar enlace a... Ingresar con oAuth2 @@ -215,9 +216,9 @@ La identidad del sitio no puede ser verificada - El certificado del servidor no es de confianza - El certificado del servidor expiró - - El certificado del servidor es de una fecha que aún no llega + - El certificado del servidor es de una fecha que aún no ha llegado - La URL no coincide con el nombre de dominio del certificado - ¿Confías de todas formas en este certificado? + ¿Confía de todas formas en este certificado? El certificado no pudo ser guardado Detalles Ocultar @@ -280,7 +281,7 @@ Cuentas Agregar cuenta La conexión segura está siendo desviada por una ruta insegura. - Logs + Registros Enviar historial No se ha encontrado una app para enviar logs. Instale la app mail! Se han encontrado %1$s logs de la app Android @@ -289,7 +290,7 @@ Contraseña incorrecta Mover Aquí no hay nada. ¡Puede agregar una carpeta! - Seleccionar + Elegir No se puede mover. Revise si el archivo existe No se puede mover una carpeta dentro de una de SUS subcarpetas. El archivo ya existe en la carpeta de destino @@ -297,5 +298,7 @@ para mover este archivo Subidas instantáneas Seguridad - Ruta de vídeo de subida + Guardar videos subidos en la carpeta: + La descarga de la carpeta %1$s no ha podido ser completada + %1$s compartió \"%2$s\" contigo diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index d8dd2639..0bca4628 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -12,6 +12,7 @@ Xehetasunak Bidali Ordenatu + Ordenatu honen arabera A-Z Berrienak - Zaharrenak @@ -38,6 +39,7 @@ Oharrak Imprint Probatu %1$s zure telefono adimentsuan! + Nik %1$s zure telefono adimentsuan erabitzera gonbidatu nahi zaitut!\nDeskargatu hemen: %2$s Egiaztatu zerbitzaria Zerbitzariaren helbidea https:// Erabiltzaile izena @@ -115,6 +117,7 @@ %1$d fitxategien edukiak ezin dira sinkronizatu (%2$d gatazka) Bertako fitxategi batzuk ahaztu dira %2$s karpetako %1$d fitxategi ezin dira dira kopiatu + 1.3.16 bertsioan, gailu honetatik igotzen diren fitxategiak bertako %1$s karpetara mugitzen dira datu galera ekiditeko fitxategi bat kontu ezberdinekin sinkronizatzen denean.\n\n Aldaketa hau dela eta, programa honen aurreko bertsioetan igotako fitxategi guztiak %2$s karpetara kopiatu dira. Hala ere, errore batek hau burutzea ekidin du kontuaren sinkronizazioa egiten ari zen bitartean. Orain fitxategiak dauden bezala utz ditzakezu eta %3$s rako lotura ezabatu, edo fitxategiak %1$s karpetara mugi ditzakezu eta %4$srako lotura mantendu.\n\nBehean bertako fitxategien zerrenda eta %5$s era lotuta zeuden urruneko fitxategiena. %1$s karpeta dagoeneko ez da existitzen Mugitu denak Fitxategi guztiak mugitu dira @@ -250,7 +253,9 @@ Mesedez, baimendu berriz %1$s ezin da %2$s karpeta lokalera kopiatu Igotzetarako Bidea Sentitzen dut, partekatzea ez dago zure zerbitzarian gaituta. Mesedez jarri harremanetan zure administratzailearekin. + Ezin izan da partekatu. Mesedez egiaztatu fitxategia existitzen dela Errore bat egon da fitxategaia edo karpeta partekatzerakoan + Ezin izan da partekatzea desegin. Mesedez egiaztatu fitxategia existitzen dela Errore bat egon da fitxategaia edo karpeta partekatzeari uzterakoan Bidali Lotura kopiatu @@ -271,10 +276,23 @@ Mesedez, baimendu berriz Fitxategia jadanik ez dago eskuragarri zerbitzarian Kontuak Gehitu kontua + Konexio segurua birbideratu da segurua ez den bide batera. + Egunkariak + Bidali Historia + Egunkariak bidaltzeko aplikaziorik ez da aurkitu. Instalatu posta aplikazioa! + %1$s Android aplikazioaren egunerokoak + Datuak kargatzen... Autentikazioa beharrezkoa Pasahitz okerra Mugitu + Hemen ez dago ezer. Karpeta bat gehi dezakezu! Aukeratu + Ezin izan da mugitu. Mesedez egiaztatu fitxategia existitzen dela + Fitxategia dagoeneko existitzen da helburuko karpetan + Errore bat gertatu da fitxategi edo karpeta hau mugitzen saiatzerakoan + fitxategi hau mugitzeko Berehalako Igoerak Segurtasuna + Bideo Igoera Bidea + %1$s karpetaren deskarga ezin izan da burutu diff --git a/res/values-fi-rFI/strings.xml b/res/values-fi-rFI/strings.xml index 4ba0fcf0..e69853b7 100644 --- a/res/values-fi-rFI/strings.xml +++ b/res/values-fi-rFI/strings.xml @@ -75,6 +75,7 @@ Päivitä tiedosto Tiedoston nimeksi muutettiin %1$s siirron yhteydessä Jaa linkki + Poista linkin jako Kyllä Ei OK @@ -130,12 +131,15 @@ Väärä sovelluksen PIN Sovelluksen PIN poistettu Sovelluksen PIN-koodi tallennettu + %1$s-musiikkisoitin %1$s (toistetaan) %1$s (ladataan) Mediatiedostoa ei löytynyt Tiliä ei määritetty Tiedosto ei ole kelvollisella tilillä + Mediakoodekki ei ole tuettu Mediatiedoston luku ei onnistunut + Mediatiedostoa ei ole koodattu kelvollisesti Aikakatkaisu toistoa yrittäessä Mediatiedostoa ei voi suoratoistaa Turvallisuusvirhe yrittäessä toistaa kohdetta %1$s @@ -151,6 +155,7 @@ Testataan yhteyttä... Väärin tehdyt palvelin-asetukset Laitteella on jo tili samalle käyttäjälle ja palvelimelle + Syötetty käyttäjä ei täsmää tämän tilin käyttäjän kanssa Tuntematon virhe Isäntää ei löydy Palvelin-instanssia ei löydetty @@ -172,6 +177,7 @@ Palvelin ei tue tätä tunnistautumistapaa %1$s ei tue useita tilejä Tunnistautuminen palvelinta vastaan ei onnistu + Tiliä ei ole olemassa vielä laitteella Pidä tiedosto ajan tasalla Nimeä uudelleen Poista @@ -271,4 +277,5 @@ Tämän tiedoston tai kansion siirtoa yrittäessä tapahtui virhe Välittömät lähetykset Tietoturva + %1$s jakoi kohteen \"%2$s\" kanssasi diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index a49a4091..f52eec64 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -12,23 +12,23 @@ Détails Envoyer Trier - Trier par + Trier - A-Z - Plus récent - Plus ancien + par ordre alphabétique + du plus récent au plus ancien Général Plus Comptes - Gestion des comptes utilisateur - Utilisation d\'un code de sécurité - Protéger l\'accès aux données manipulées par le client - Téléchargements instantanés d\'images - Téléversement instantané des photos prises par la caméra - Téléchargements instantanés de vidéos - Téléversement instantané des vidéos prises par la caméra + Gestion des comptes + Code de sécurité + Protéger l\'accès à l\'application + Téléversement immédiat des photos + Téléverser immédiatement les photos prises par la caméra + Téléversement immédiat des vidéos + Téléverser immédiatement les vidéos prises par la caméra Activer les logs Utilisé pour enregistrer les problèmes dans les logs Historique des logs @@ -39,27 +39,27 @@ Commentaires Empreinte Mémoriser l\'emplacement de partage - Mémoriser le dernier emplacement d\'upload - Essayez %1$s sur votre smartphone&nbsp;! + Mémoriser le dernier emplacement de téléversement + Essayez %1$s sur votre smartphone ! J\'aimerais vous inviter à utiliser %1$s sur votre smartphone ! Téléchargez-le ici : %2$s Vérifier le serveur Adresse du serveur https://… Nom d\'utilisateur Mot de passe - Nouveau dans %1$s&nbsp;? + Nouveau dans %1$s ? Fichiers Connecter Téléverser Sélectionner le dossier d\'envoi : Aucun compte n\'a été trouvé Aucun compte %1$s n\'a été trouvé. Veuillez commencer par en configurer un. - Paramètres + Configuration Quitter Rien à envoyer Aucun contenu reçu. Rien à envoyer. %1$s n\'est pas autorisé à accéder au contenu partagé - Téléversement + Téléversement... il y a quelques secondes Il n\'y a rien ici ! Envoyez donc quelque chose :) Chargement… @@ -68,7 +68,7 @@ Téléchargez-le ici : %2$s dossiers fichier fichiers - Effleurez un fichier pour afficher les informations complémentaires + Effleurez un fichier pour afficher les informations complémentaires. Taille : Type : Créé le : @@ -82,7 +82,7 @@ Téléchargez-le ici : %2$s Non OK Annuler le téléchargement - Annuler l\'envoi + Annuler le téléversement Annuler Sauvegarder & Quitter Erreur @@ -90,14 +90,14 @@ Téléchargez-le ici : %2$s Erreur inconnue À propos de Changer de mot de passe - Effacer ce compte + Supprimer ce compte Créer un compte Téléverser un fichier depuis… Nom du dossier Téléversement… Envoi du fichier %2$s : %1$d%% effectués Téléversement réussi - Le fichier %1$s a été envoyé avec succès + Le fichier %1$s a été téléversé avec succès Échec de l\'envoi L\'envoi de %1$s a échoué Le téléversement a échoué, vous devez vous connecter à nouveau @@ -111,31 +111,31 @@ Téléchargez-le ici : %2$s Le téléchargement a échoué, vous devez vous connecter à nouveau Choisissez un compte La synchronisation a échoué - Échec de la synchronisation, vous devez vous reconnecter à nouveau - La synchronisation de %1$s ne peut pas être complétée - Mot de passe invalide pour %1$s + Échec de la synchronisation, vous devez vous reconnecter + La synchronisation de %1$s n\'a pu être terminée + Mot de passe non valide pour %1$s Des conflits ont été trouvés - %1$d fichiers à garder synchronisés n\'ont put être synchronisé + %1$d fichiers à garder synchronisés n\'ont pu être synchronisés La synchronisation des fichiers a échoué Le contenu de %1$d fichiers n\'a pu être synchronisé (%2$d conflits) Certains fichiers locaux ont été oubliés %1$d fichiers du dossier %2$s n\'ont pas pu être copiés dans - Depuis la version 1.3.16, les fichiers envoyé depuis ce périphérique sont copiés dans le dossier local %1$s pour éviter une perte de données lorsqu\'un même fichier est synchronisé avec plusieurs comptes. + Depuis la version 1.3.16, les fichiers envoyés depuis ce périphérique sont copiés dans le dossier local %1$s pour éviter une perte de données lorsqu\'un même fichier est synchronisé avec plusieurs comptes. -En raison de cette modification, tous les fichiers envoyés avec des versions antérieures de cette application ont été copiés dans le dossier %2$s. Cependant une erreur a empêché l\'achèvement de cette opération pendant la synchronisation du compte. Vous pouvez soit laisser les fichiers tels quels et supprimer le lien vers %3$s, soit déplacer les fichiers dans le dossier %1$s et garder le lien vers %4$s. +En raison de cette modification, tous les fichiers envoyés avec des versions antérieures de cette application ont été copiés dans le dossier %2$s. Cependant, une erreur a empêché l\'achèvement de cette opération pendant la synchronisation du compte. Vous pouvez soit laisser les fichiers tels quels et supprimer le lien vers %3$s, soit déplacer les fichiers dans le dossier %1$s et garder le lien vers %4$s. Ci-dessous la liste des fichiers locaux, et les fichiers distants dans %5$s auxquels ils étaient liés. Le dossier %1$s n\'existe plus Tout déplacer Tous les fichiers ont été déplacés Certains fichiers n\'ont pu être déplacés - Local&nbsp;: %1$s + Local : %1$s Distant : %1$s - Il n\'y a pas assez de place disponible pour copier les fichiers sélectionnés dans le dossier %1$s. Voulez-vous quand même les déplacer ? + Il n\'y a pas assez de place disponible pour copier les fichiers sélectionnés dans le dossier %1$s. Voulez-vous les déplacer à la place ? Veuillez saisir votre code de sécurité Veuillez saisir votre code de sécurité - Le code PIN vous sera demandé à chaque lancement de l\'application - Veuillez saisir à nouveau votre code de sécurité + Le code de sécurité vous sera demandé à chaque lancement de l\'application + Veuillez saisir de nouveau votre code de sécurité Retirer le code de sécurité Les deux codes saisis ne concordent pas Code de sécurité incorrect @@ -151,7 +151,7 @@ Ci-dessous la liste des fichiers locaux, et les fichiers distants dans %5$s auxq Le codec de ce média n\'est pas pris en charge Le fichier média ne peut pas être lu Le fichier média n\'est pas correctement encodé - Délai dépassé pour la lecture du morceau. + Délai dépassé pour la lecture du morceau Le fichier média ne peut pas être diffusé Le fichier média ne peut être joué avec le lecteur standard Erreur de sécurité à la lecture de %1$s @@ -167,13 +167,13 @@ Ci-dessous la liste des fichiers locaux, et les fichiers distants dans %5$s auxq Connexion établie Test de la connexion… Configuration du serveur erronée - Un compte pour le même utilisateur et serveur existe déjà sur ce périphérique + Un compte pour le même utilisateur et serveur existe déjà sur cet appareil L\'utilisateur entré ne correspond pas à l\'utilisateur de ce compte - Une erreur inconnue s\'est produite + Une erreur inconnue s\'est produite. Impossible de trouver l\'hôte Aucune instance du serveur n\'a été trouvée - Le serveur met trop longtemps à répondre - Adresse invalide + Le serveur a pris trop de temps à répondre + Adresse non valide Échec de l\'initialisation SSL Impossible de vérifier l\'identité du serveur SSL La version du serveur n\'est pas reconnue @@ -192,29 +192,30 @@ Ci-dessous la liste des fichiers locaux, et les fichiers distants dans %5$s auxq Votre serveur a retourné un identifiant d\'utilisateur incorrect. Veuillez prendre contact avec votre administrateur Impossible de s\'authentifier sur ce serveur + Le compte n\'existe pas encore sur ce périphérique Maintenir le fichier à jour Renommer Supprimer - Voulez-vous vraiment supprimer %1$s&nbsp;? - Voulez-vous vraiment supprimer %1$s et son contenu&nbsp;? + Voulez-vous vraiment supprimer %1$s ? + Voulez-vous vraiment supprimer %1$s et son contenu ? Local seulement - Le contenu local uniquement + Contenu local uniquement Effacer du serveur - Les deux distant et local + Distant et local Suppression effectuée avec succès Suppression impossible Entrez un nouveau nom La version locale ne peut être renommée, veuillez réessayer avec un nom différent Renommage impossible Le fichier distant n\'a pu être vérifié - Le contenu des fichiers est déjà synchronisé + Le contenu du fichier est déjà synchronisé Le dossier n\'a pas pu être créé - Caractères interdits&nbsp;: / \\ &lt; &gt; : " | ? * + Caractères interdits : / \\ < > : \" | ? * Le nom du fichier ne peut pas être vide Veuillez patienter Problème inattendu. Veuillez essayer une autre application pour la sélection du fichier Aucun fichier sélectionné - Envoyer un lien à… + Envoyer le lien vers… Connexion avec oAuth2 Connexion au serveur oAuth2… L\'identité du site ne peut être vérifiée @@ -222,23 +223,23 @@ Ci-dessous la liste des fichiers locaux, et les fichiers distants dans %5$s auxq - Le certificat du serveur a expiré - Le certificat du serveur n\'est pas encore valide - L\'URL ne correspond pas au nom d\'hôte du certificat - Voulez-vous tout de même faire confiance à ce certificat&nbsp;? + Voulez-vous tout de même faire confiance à ce certificat ? Impossible de sauvegarder le certificat Détails Masquer - Délivré à&nbsp;: - Délivré par&nbsp;: + Délivré à : + Délivré par : Nom d\'usage : - Organisation&nbsp;: - Unité organisationnelle&nbsp;: - Pays&nbsp;: - Région&nbsp;: - Localisation&nbsp;: - Validité&nbsp;: - De&nbsp;: - À&nbsp;: - Signature&nbsp;: - Algorithme&nbsp;: + Organisation : + Unité organisationnelle : + Pays : + Région : + Localisation : + Validité : + Du : + Au : + Signature : + Algorithme : Impossible d\'afficher le certificat. - Aucune information sur l\'erreur Ceci est un espace réservé @@ -249,16 +250,16 @@ Ci-dessous la liste des fichiers locaux, et les fichiers distants dans %5$s auxq 12:23:45 Téléverser les images via une connexion WiFi uniquement Téléverser les vidéos via une connexion WiFi uniquement - /TéléversementInstantané + /InstantUpload Conflit de mise à jour - Le fichier distant %s n\'est pas synchronisé avec le fichier local. En choisissant de continuer, vous remplacerez le contenu de fichier sur le serveur. + Le fichier distant %s n\'est pas synchronisé avec le fichier local. En choisissant de continuer, vous remplacerez le contenu du fichier sur le serveur. Garder les deux versions Écraser Ne pas téléverser Prévisualisation de l\'image Cette image ne peut pas être affichée %1$s n\'a pas pu être copié dans le dossier local %2$s - Chemin d\'accès pour le téléversement + Répertoire de téléversement Désolé, le partage n\'est pas disponible sur votre serveur. Veuillez contacter votre administrateur. Impossible de partager. Vérifiez que le fichier est bien présent Une erreur est survenue lors de la tentative de partage de ce fichier ou répertoire @@ -267,11 +268,11 @@ Ci-dessous la liste des fichiers locaux, et les fichiers distants dans %5$s auxq Envoyer Copier le lien Copié dans le presse-papiers - Erreur critique&nbsp;: impossible de réaliser des opérations - Une erreur s\'est produite pendant la connection au serveur - Une erreur est survenue pendant l\'attente du serveur. L\'opération n\'a pas pu être effectuée. - Une erreur est survenue pendant l\'attente du serveur. L\'opération n\'a pas pu être effectuée. - L\'opération n\'a pas pu être terminée, le serveur n\'est pas disponible. + Erreur critique : impossible de réaliser des opérations + Une erreur est survenue pendant la connexion au serveur. + Une erreur est survenue pendant l\'attente du serveur. L\'opération n\'a pas pu être effectuée + Une erreur est survenue pendant l\'attente du serveur. L\'opération n\'a pas pu être effectuée + L\'opération n\'a pas pu être terminée, le serveur n\'est pas disponible Vous ne possédez pas les droits suffisants %s afin de renommer ce fichier @@ -283,10 +284,12 @@ Ci-dessous la liste des fichiers locaux, et les fichiers distants dans %5$s auxq Ce fichier n’est plus disponible sur le serveur Comptes Ajouter un compte - La connexion sécurisée est redirigée via une route non-sécurisée. + Le connexion sécurisée est redirigée vers une route non-sécurisée. Journaux Envoyer l\'historique - Chargement des données... + Aucune application trouvée pour l\'envoi de journaux. Installer une application de courriel ! + Journaux de l\'application Android %1$s + Chargement des données… Authentification requise Mot de passe incorrect Déplacer @@ -297,7 +300,9 @@ Ci-dessous la liste des fichiers locaux, et les fichiers distants dans %5$s auxq Le fichier existe déjà dans le dossier de destination Une erreur est survenue lors de la tentative de déplacement de ce fichier ou dossier de déplacer ce fichier - Téléchargements instantanés + Envois immédiats Sécurité - Chemin d\'accès pour le téléversement + Répertoire de téléversement des vidéos + Le téléchargement du dossier %1$s n\'a pas pu être achevé + %1$s a partagé \"%2$s\" avec vous diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml index 68d5d8c3..882f17ca 100644 --- a/res/values-gl/strings.xml +++ b/res/values-gl/strings.xml @@ -8,7 +8,7 @@ Ficheiros Abrir con Novo cartafol - Preferencias + Axustes Detalles Enviar Ordenar @@ -188,6 +188,7 @@ Descárgueo de aquí: %2$s O seu servidor non devolveu un ID de usuario correcto, contacte cun administrador Non pode autenticarse neste servidor + Aínda non existe a conta no dispositivo Manter actualizado o ficheiro Renomear Retirar @@ -283,6 +284,9 @@ Descárgueo de aquí: %2$s A conexión segura está a ser redirixida a unha ruta non segura. Rexistros Enviar o historial + Non se atopou unha aplicación para enviar os rexistros. Instale unha aplicación de correo! + Rexistros da aplicación %1$s Android + Cargando os datos... Requírese autenticación Contrasinal incorrecto Mover @@ -296,4 +300,6 @@ Descárgueo de aquí: %2$s Envío instantáneo Seguranza Enviar a ruta do vídeo + Non foi posíbel completar a descarga do cartafol %1$s + %1$s compartiu «%2$s» con vostede diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml index 173239c4..eb8bf880 100644 --- a/res/values-hr/strings.xml +++ b/res/values-hr/strings.xml @@ -1,15 +1,34 @@ + %1$s Android aplikacija + verzija %1$s + Osvježi račun Učitaj + Sadržaj iz drugih aplikacija Datoteke + Otvori sa Nova mapa Postavke + Detalji Pošaljite + Sortiraj + Sortiraj po + + A-Z + Najnoviji- Stariji + Općenito više Korisnićki računi + Upravljaj računima + PIN aplikacije + Zaštit svog klijenta + Trenutni upload slika + Trenutni upload slika snimljenih kamerom + Trenutni upload videa + Trenutni upload videa snimljen kamerom Pomoć Korisničko ime Lozinka @@ -37,6 +56,7 @@ Trying to login… Promjeni ime Makni + Detalji Pošaljite Korisnićki računi diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 94fc138e..c4f396f3 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -187,6 +187,7 @@ Il tuo server non ha restituito un id utente corretto, contatta un amministratore Impossibile eseguire l\'autenticazione su questo server + L\'account non esiste ancora sul dispositivo Tieni aggiornato il file Rinomina Rimuovi @@ -298,4 +299,6 @@ Caricamenti istantanei Protezione Percorso di caricamento video + Lo scaricamento della cartella %1$s non può essere completato + %1$s ha condiviso \"%2$s\" con te diff --git a/res/values-ja-rJP/strings.xml b/res/values-ja-rJP/strings.xml index cbc84bb5..e1e1e343 100644 --- a/res/values-ja-rJP/strings.xml +++ b/res/values-ja-rJP/strings.xml @@ -64,8 +64,8 @@ ここには何もありません。何かアップロードしてください。 読込中 ... このフォルダーにはファイルがありません。 - フォルダ - フォルダ + フォルダー + フォルダー ファイル ファイル ファイルをタップすると追加情報が表示されます。 @@ -185,9 +185,10 @@ 認証サーバーに接続中 ... サーバーはこの認証方式をサポートしていません %1$s は複数アカウントをサポートしていません - サーバーが正しいユーザーIDを返しませんでした。管理者にご連絡ください。 + サーバーが正しいユーザーIDを返しませんでした。管理者に連絡してください。 このサーバーに対して認証できません + デバイス上にまだアカウントが存在しません ファイルを最新に保つ 名前を変更 削除 @@ -243,7 +244,7 @@ 389 KB 2012/05/18 12:23 PM 12:23:45 - WiFi経由でのみ写真をアップロード + WiFi経由でのみ画像をアップロード WiFi経由でのみ動画をアップロード /InstantUpload 更新が競合 @@ -255,8 +256,8 @@ この画像は表示できません %1$s は、ローカルフォルダー %2$s にコピーできませんでした。 アップロードパス - 申し訳ございません。共有がサーバー上で有効になっていません。 管理者に - ご連絡ください。 + すみませんが、サーバーで共有が有効になっていません。 + 管理者に連絡してください。 共有できません。ファイルがあるか確認してください。 このファイルまたはフォルダーを共有する際にエラーが発生しました 共有を解除できません。ファイルがあるか確認してください。 @@ -283,17 +284,22 @@ 暗号化接続は非暗号化接続にリダイレクトされました。 ログ ログを送信 + ログを送信するアプリが見つかりませんでした。メールアプリをインストールしてください。 + %1$s アンドロイドアプリログ + 読込中 ... 認証を必要とする 無効なパスワード 移動 - ファイルが有りません。フォルダを追加してください。 + 何もありません。フォルダーを追加してください。 選択 移動できません。ファイルがあるか確認してください。 - フォルダを子フォルダへ移動することはできません。 - そのファイルは、宛先フォルダに既に存在しています。 + フォルダーを子フォルダーへ移動することはできません。 + そのファイルは宛先フォルダーにすでに存在します。 このファイルまたはフォルダーを移動する際にエラーが発生しました このファイルを移動 自動アップロード セキュリティ 動画のアップロードパス + %1$s のフォルダのダウンロードが完了しませんでした。 + %1$sがあなたと\"%2$s\"を共有しました diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml index 36bbbcf9..cc48be13 100644 --- a/res/values-km/strings.xml +++ b/res/values-km/strings.xml @@ -1,18 +1,37 @@ + %1$s កម្មវិធីអានដ្រយ + ជំនាន់ %1$s + គណនីឱ្យថ្មីឡើងវិញ ផ្ទុក​ឡើង + មាតិការ​ពីកម្មវិធីផ្សេងទៀត ឯកសារ + បើកជាមួយ ថត​ថ្មី ការកំណត់ ព័ត៌មាន​លម្អិត ផ្ញើ + តម្រៀប + តម្រៀបដោយ + + A-Z + ថ្មីបំផុត-ចាស់បំផុត + ទូទៅ ច្រើន​ទៀត គណនី គ្រប់គ្រង​គណនី + ភីន​កូដ កម្មវិធី + ដំណើរការការចូលទៅកាន់ + នេះជាបញ្ហា​សម្រាប់​អ្នក​ដែល​បាន​ចូលទៅកាន់ + ប្រវត្តិនៃការចូលទៅកាន់ + នៅទីនេះ​គឺបង្ហាញការដែលបាន​ចូលទៅកាន់ + លុប​ប្រវត្តិ ជំនួយ + ផ្ដល់អនុសាសន៍ទៅកាន់មិត្តភក្ដិ + មតិត្រឡប់ ឈ្មោះ​អ្នកប្រើ ពាក្យសម្ងាត់ ឯកសារ @@ -41,6 +60,7 @@ កំហុស កំពុងដំណើរការ មិន​ស្គាល់​កំហុស + អំពី ប្តូរ​ពាក្យសម្ងាត់ លប់គណនី បង្កើតគណនី @@ -69,6 +89,9 @@ រក្សាឯកសាររហូតដល់កាលបរិច្ឆេទ ប្ដូរ​ឈ្មោះ ដកចេញ + ទីកន្លែងតែមួយ + ដកចេញពី​សឺវឺ + បញ្ជារ និងទីតាំង ការដកយកចេញបានជោគជ័យ ការដកយកចេញបានបរាជ័យ បញ្ចូលឈ្មោះថ្មី diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml index 69623e19..f55428eb 100644 --- a/res/values-kn/strings.xml +++ b/res/values-kn/strings.xml @@ -1,6 +1,35 @@ + ಪೇರಿಸು + ಕಡತಗಳು + ಹೊಸ ಕಡತಕೋಶ + ಆಯ್ಕೆ + ಕಳುಹಿಸಿ + ಇನ್ನಷ್ಟು + ಸಹಾಯ + ಮುದ್ರೆ + ಬಳಕೆಯ ಹೆಸರು + ಗುಪ್ತ ಪದ + ಕಡತಗಳು + ಪೇರಿಸು + ಪ್ರತಿಯನ್ನು ಸ್ಥಳೀಯವಾಗಿ ಉಳಿಸಿಕೊಳ್ಳಿ + ಸಂಪರ್ಕ ಕೊಂಡಿಯನ್ನು ಹಂಚಿಕೊಳ್ಳಬಹುದು + ಹೌದು + ಇಲ್ಲ + ಸರಿ + ವರ್ಗಾವಣೆ ರದ್ದು ಮಾಡಿ + ರದ್ದು + ತಪ್ಪಾಗಿದೆ + ಗೊತ್ತಿಲ್ಲದ ದೋಷ + ಗುಪ್ತ ಪದವನ್ನು ಬದಲಾಯಿಸಿ + ಮರುಹೆಸರಿಸು + ತೆಗೆದುಹಾಕಿ + ಕಳುಹಿಸಿ + ದೃಢೀಕರಣ ಅಗತ್ಯವಿದೆ + ದುರ್ಬಲ ಗುಪ್ತಪದ + ಆಯ್ಕೆ + ಭದ್ರತೆ diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index 3bb9a60c..b58f7983 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -6,29 +6,42 @@ 업로드 다른 앱의 콘텐츠 파일 - 로 열기 + 다음으로 열기 새 폴더 설정 - 세부내용 + 자세한 정보 보내기 + 정렬 + 정렬 순서 + + 가나다 + 최신 - 이전 + 일반 - 더 중요함 + 더 보기 계정 계정 관리 앱 암호 내 클라이언트 보호 - 로깅 허용 - 이건 로그 문제에 사용됩니다 + 사진 즉시 업로드 + 카메라로 찍은 사진 즉시 업로드 + 동영상 즉시 업로드 + 카메라로 찍은 동영상 즉시 업로드 + 로그 기록 사용 + 문제점을 기록하는 데 사용됩니다 로그 기록 여기서 기록된 로그를 보여줍니다 - 역사 삭제하기 + 과거 기록 삭제 도움말 - 친구들에게 권하기 + 친구에게 추천하기 피드백 - 임프린트 - %1$s 을 스마트폰에서 사용해보세요! + 법적 고지 + 공유 위치 기억하기 + 마지막 공유 업로드 위치 기억하기 + %1$s을(를) 스마트폰에서 사용해 보세요! + %1$s을(를) 스마트폰에서 사용해 보는 것을 추천합니다!\n다운로드 링크: %2$s 서버 확인 서버 주소 https://… 사용자 이름 @@ -37,6 +50,7 @@ 파일 접속 업로드 + 업로드 폴더 선택: 계정 없음 이 장치에 %1$s 계정이 없습니다. 먼저 계정을 설정하십시오. 설정 @@ -45,8 +59,10 @@ 받은 콘텐츠가 없습니다. 업로드할 항목이 없습니다. %1$s에서 공유된 콘텐츠에 접근할 수 없습니다 업로드 중 - 초 전 + 초 지남 내용이 없습니다. 업로드할 수 있습니다! + 불러오는 중... + 이 폴더에 파일이 없습니다. 폴더 폴더 파일 @@ -57,9 +73,10 @@ 만든 날짜: 수정한 날짜: 다운로드 - 파일 새로고침 + 파일 새로 고침 업로드 중 파일 이름을 %1$s(으)로 변경하였습니다 링크 공유 + 링크 공유 해제 예 아니요 확인 @@ -69,7 +86,7 @@ 저장하고 끝내기 오류 불러오는 중... - 알수없는 오류 + 알 수 없는 오류 정보 암호 변경 계정 삭제 @@ -81,29 +98,35 @@ 업로드 성공 %1$s을(를) 업로드하였습니다 업로드 실패 - %1$s을(를) 업로드할 수 없었습니다 + %1$s을(를) 업로드할 수 없습니다 + 업로드가 실패하였습니다. 다시 로그인하십시오 다운로드 중... %1$d%% %2$s 다운로드 중 다운로드 성공 %1$s을(를) 다운로드하였습니다 다운로드 실패 - %1$s을(를) 다운로드할 수 없었습니다 + %1$s을(를) 다운로드할 수 없습니다 아직 다운로드 되지 않았습니다 + 다운로드가 실패하였습니다. 다시 로그인하십시오 계정 선택 동기화 실패 + 동기화가 실패하였습니다. 다시 로그인하십시오 %1$s와(과) 동기화할 수 없었습니다 - %1$s에 대한 비밀번호가 틀립니다 + %1$s의 암호가 올바르지 않습니다 충돌하는 항목 발견됨 - 동기화된 파일 중 %1$d개를 동기화할 수 없었습니다 + 동기화된 파일 중 %1$d개를 동기화할 수 없습니다 파일을 동기화할 수 없었습니다 - 파일 %1$d개의 내용을 동기화할 수 없었습니다 (충돌 %2$d개) - 몇몇 로컬 파일이 사라졌습니다. - %1$s 폴더가 존재하지 않습니다. - 모두 옮김 - 모든 파일 옮김 - 몇몇 파일을 옮기지 못했습니다. + 파일 %1$d개의 내용을 동기화할 수 없습니다 (충돌 %2$d개) + 일부 로컬 파일이 사라졌습니다. + 폴더 %2$s의 파일 중 %1$d개를 복사할 수 없습니다 + 버전 1.3.16부터는 하나의 파일이 여러 계정과 동기화될 때 데이터 손실을 막기 위해서 이 장치에서 업로드된 파일은 로컬 폴더 %1$s(으)로 복사됩니다.\n\n이 변경 사항 때문에 이 앱의 이전 버전에서 업로드된 모든 파일은 폴더 %2$s(으)로 복사되었습니다. 계정 동기화 중 오류가 발생하여 이 작업이 중단되었습니다. 파일을 그대로 둔 다음 %3$s(으)로 향한 링크를 삭제하거나, 파일을 직접 폴더 %1$s(으)로 이동한 다음 %4$s(으)로 향한 링크를 그대로 두십시오.\n\n아래 목록은 로컬 파일과 링크가 걸려 있는 %5$s에 있는 원격 파일입니다. + 폴더 %1$s이(가) 더 이상 존재하지 않습니다. + 모두 이동 + 모든 파일 이동됨 + 몇몇 파일을 이동할 수 없음 로컬: %1$s 원격: %1$s + 선택한 파일을 폴더 %1$s(으)로 복사할 공간이 부족합니다. 파일을 이동하시겠습니까? 앱 암호를 입력하십시오 앱 암호를 입력하십시오 앱을 시작할 때마다 암호를 물어봅니다 @@ -114,79 +137,89 @@ 앱 암호가 삭제되었습니다 앱 암호가 저장되었습니다 %1$s 음악 재생기 - %1$s (재생중) + %1$s (재생 중) %1$s (불러오는 중) %1$s 재생 완료됨 - 미디어 파일을 찾을수 없습니다 + 미디어 파일을 찾을 수 음 준비된 계정이 없습니다 유효한 계정의 파일이 아닙니다 지원하지 않는 미디어 코덱 - 미디어 파일을 읽을수 + 미디어 파일을 읽을 수 없음 미디어 파일이 제대로 인코드 되지 않았습니다 재생 시도 중 시간이 초과됨 - 미디어 파일을 스트리밍 할수 없습니다 - 내장된 미디어 플레이어에서는 이 미디어 파일을 재생할수 없습니다 - %1$s 를 재생하는 중에 보안오류가 발생함 - %1$s 를 재생하는 중에 입력 에러가 발생함 - %1$s 를 재생하던 중에 알수 없는 오류가 발생함 - 되감기 버튼 - 재생 혹은 일시정지 버튼 - 빨리감기 버튼 - 로그인 중... + 미디어 파일을 스트리밍 할 수 없습니다 + 내장된 미디어 플레이어에서 이 미디어 파일을 재생할 수 없습니다 + %1$s을(를) 재생하는 중 보안 오류가 발생함 + %1$s을(를) 재생하는 중 입력 오류가 발생함 + %1$s을(를) 재생하는 중 알 수 없는 오류가 발생함 + 되감기 단추 + 재생 혹은 일시 정지 단추 + 빨리감기 단추 + 인증 정보 가져오는 중... + 로그인 시도 중... 네트워크에 연결할 수 없습니다 암호화된 연결을 사용할 수 없습니다. 연결됨 연결 테스트 중... 서버 설정이 잘못됨 같은 사용자와 서버에 대한 계정이 이미 존재합니다 - 입력된 사용자가 이 계정의 사용자와 일치하지 않음 + 입력된 사용자가 이 계정의 사용자와 일치하지 않습니다 알 수 없는 오류가 발생하였습니다! 호스트를 찾을 수 없음 서버 인스턴스를 찾을 수 없음 - 서버 응답 시간이 초과되었습니다 + 서버 응답 시간이 초과됨 잘못된 URL SSL 초기화 오류 SSL 서버의 신원을 확인할수 없습니다 확인할 수 없는 서버 버전 연결을 수립할 수 없음 암호화된 연결 사용 중 - 잘못된 로그인/암호 - 권한부여가 성공적으로 이뤄지지 않았습니다 - 권한 서버로 부터 접근이 거부되었습니다 - 뜻밖의 상태; 다시 서버 주소를 입력해주십시오 - 인증이 만료되었습니다. 다시 인증해주세요 - 현재 암호를 - 세션이 만료되었습니다. 다시 접속해주세요 - 인증 서버에 접속하는 중... + 잘못된 사용자 이름 및 암호 + 인증 실패 + 인증 서버 접근 거부됨 + 예상하지 못한 상태입니다. 서버 URL을 다시 입력해 주십시오 + 인증이 만료되었습니다. 다시 인증해 주십시오 + 현재 암호를 입력해 주십시오 + 세션이 만료되었습니다. 다시 접속해 주십시오 + 인증 서버에 연결하는 중... 서버에서 이 인증 방법을 지원하지 않습니다. - %1$s 에서는 다중 계정을 지원하지 않습니다 + %1$s에서 다중 계정을 지원하지 않습니다 + 서버에서 올바른 사용자 ID를 반환하지 않았습니다. 관리자에게 연락하십시오 + + 이 서버에 인증할 수 없음 + 장치에 아직 계정이 존재하지 않습니다. 파일을 최신 정보로 유지 이름 바꾸기 삭제 + %1$s을(를) 삭제하시겠습니까? + %1$s 및 포함된 내용을 삭제하시겠습니까? 로컬만 로컬 콘텐츠만 서버에서 삭제 서버와 로컬 모두 - 성공적으로 삭제하였습니다 - 삭제할 수 없었습니다 + 성공적으로 삭제함 + 삭제할 수 없음 새 이름 입력 로컬 파일의 이름을 변경할 수 없습니다. 다른 이름을 입력하십시오 - 이름을 변경할 수 없었습니다 - 원격 파일을 확인할 수 없었습니다 - 파일 내용이 이미 동기화되었습니다 - 사용할수 없는 문자들: / \\ < > : \" | ? * + 이름을 변경할 수 없음 + 원격 파일을 확인할 수 없음 + 파일 내용이 이미 동기화됨 + 폴더를 만들 수 없음 + 사용할 수 없는 문자: / \\ < > : \" | ? * + 파일 이름이 비어 있을 수 없음 잠시 기다려 주십시오 예상하지 못한 오류입니다. 다른 앱에서 파일을 선택하십시오 선택한 파일 없음 + 다음으로 링크 보내기... oAuth2로 로그인하기 - oAuth2 서버에 연결중... - 사이트 인증서를 확인할 수 없었습니다 + oAuth2 서버에 연결 중... + 사이트 인증서를 확인할 수 없습니다 - 서버 인증서를 신뢰할 수 없습니다 - 서버 인증서가 만료되었습니다 - 서버 인증서의 유효 기간이 시작되지 않았습니다 - 인증서의 URL과 입력한 URL이 일치하지 않습니다 이 인증서를 신뢰하시겠습니까? - 인증서를 저장할 수 없었습니다 + 인증서를 저장할 수 없습니다 자세히 숨기기 발급 대상: @@ -202,27 +235,69 @@ 끝: 서명: 알고리즘: - 이것은 플레이스홀더입니다 + 인증서를 표시할 수 없습니다. + - 오류에 대한 정보가 없습니다 + 이것은 자리 비움자입니다 placeholder.txt PNG 그림 389 KB 2012/05/18 12:23 PM 12:23:45 - WiFi 사용 중일때만 사진 업로드 + Wi-Fi 사용 중일때만 사진 업로드 + Wi-Fi 사용 중일때만 동영상 업로드 /InstantUpload 업데이트 충돌 원격 파일 %s이(가) 로컬 파일과 동기화되지 않았습니다. 계속 진행하면 서버에 있는 파일을 덮어씁니다. 모두 저장 덮어쓰기 업로드하지 않음 - 그림 미리보기 + 사진 미리 보기 + 이 사진을 미리 볼 수 없습니다 + %1$s을(를) 로컬 폴더 %2$s(으)로 복사할 수 없습니다 + 업로드 경로 + 서버에서 공유가 비활성화되어 있습니다. 관리자에게 연락하십시오. + 공유할 수 없습니다. 파일이 있는지 확인하십시오 + 이 파일이나 폴더를 공유하는 중 오류 발생 + 공유를 해제할 수 없습니다. 파일이 있는지 확인하십시오 + 이 파일이나 폴더의 공유를 해제하는 중 오류 발생 보내기 - 링크 복사 + 링크 주소 복사 클립보드로 복사됨 + 치명적 오류: 작업을 진행할 수 없음 + 서버에 연결하는 중 오류가 발생하였습니다. + 서버를 기다리는 중 오류가 발생하였습니다. 작업이 진행되지 않았을 수도 있습니다 + 서버를 기다리는 중 오류가 발생하였습니다. 작업이 진행되지 않았을 수도 있습니다 + 서버를 사용할 수 없어서 작업을 진행할 수 없습니다 + %s 권한이 없습니다 + 이 파일의 이름을 바꿀 + 이 파일을 삭제할 + 이 파일을 공유할 + 이 파일의 공유를 해제할 + 파일을 생성할 + 이 폴더에 업로드할 + 이 파일을 서버에서 더 이상 사용할 수 없습니다 계정 + 계정 추가 + 보안 연결이 안전하지 않은 경로로 넘어갑니다. + 로그 + 과거 기록 보내기 + 로그를 보낼 앱이 없습니다. 메일 앱을 설치하십시오! + %1$s Android 앱 로그 + 데이터 불러오는 중... 인증 필요함 잘못된 암호 + 이동 + 항목이 없습니다. 폴더를 추가할 수 있습니다! 선택 + 이동할 수 없습니다. 파일이 존재하는 지 확인하십시오 + 폴더를 하위 폴더 아래로 이동할 수 없습니다 + 파일이 이미 대상 폴더에 존재합니다 + 이 파일이나 폴더를 이동하는 중 오류가 발생하였습니다 + 이 파일을 이동할 + 즉시 업로드 보안 + 동영상 업로드 경로 + %1$s 폴더를 다운로드할 수 없습니다 + %1$s에서 \"%2$s\"를 당신과 공유하였습니다. diff --git a/res/values-large-land/bools.xml b/res/values-large-land/bools.xml index 9feccd8a..09b11cc0 100644 --- a/res/values-large-land/bools.xml +++ b/res/values-large-land/bools.xml @@ -2,7 +2,7 @@ + + diff --git a/res/values-lt-rLT/strings.xml b/res/values-lt-rLT/strings.xml index 30cca384..76e0e523 100644 --- a/res/values-lt-rLT/strings.xml +++ b/res/values-lt-rLT/strings.xml @@ -11,6 +11,12 @@ Nustatymai Informacija Siųsti + Rikiuoti + Rikiuoti pagal + + A-Z + Naujausi - Seniausi + Bendras @@ -32,6 +38,8 @@ Rekomenduoti draugui Atsiliepimai Imprint + Prisiminti bendrinimo vietą + Prisiminti paskutinio bendrinimo įkėlimo vietą Išbandykite %1$s savo išmaniajame telefone! Patikrinti Serverį Serverio adresas diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml index c185f73f..0895ca91 100644 --- a/res/values-lv/strings.xml +++ b/res/values-lv/strings.xml @@ -149,6 +149,7 @@ Sūtīt Konti + Nepareiza parole Izvēlieties Drošība diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml index 69623e19..b4185fd3 100644 --- a/res/values-mn/strings.xml +++ b/res/values-mn/strings.xml @@ -1,6 +1,17 @@ + Байршуулах + Файлууд + Тохиргоо + Ерөнхий + Хэрэглэгчийн нэр + Нууц үг + Файлууд + Байршуулах + Аккаунт үүсгэх + Устгах + Аюулгүй байдал diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml new file mode 100644 index 00000000..69623e19 --- /dev/null +++ b/res/values-mr/strings.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/res/values-nb-rNO/strings.xml b/res/values-nb-rNO/strings.xml index 98f4ff6b..3336ec12 100644 --- a/res/values-nb-rNO/strings.xml +++ b/res/values-nb-rNO/strings.xml @@ -11,6 +11,12 @@ Innstillinger Detaljer Send + Sorter + Sorter etter + + A-Z + Nyeste - Eldste + Generelt @@ -32,6 +38,8 @@ Anbefal til en venn Tilbakemelding Avtrykk + Husk delt plassering + Husk sist delt plassering for opplasting Prøv %1$s på smarttelefonen din! Jeg ønsker å invitere deg til å bruke %1$s på smarttelefonen din!\nLast ned her: %2$s Sjekk server @@ -245,6 +253,7 @@ Bildeforhåndsvisning Dette bildet kan ikke vises %1$s kunne ikke kopieres til lokal mappe %2$s + Sti til opplasting Beklager, deling er ikke skrudd på for din tjener. Ta kontakt med administratoren. Kan ikke dele. Sjekk om filen eksisterer. @@ -270,8 +279,12 @@ Filen finnes ikke på serveren lenger Kontoer Legg til en konto + Sikker forbindelse er omdirigert til en usikker rute. Logger Send historikk + Ingen app for sending av logger funnet. Installer epost-app! + %1$s Android app logger + Laster data... Autentisering kreves Feil passord Flytt @@ -282,5 +295,8 @@ Filen finnes allerede i målmappen En feil oppstod ved flytting av denne filen eller mappen å flytte denne filen + Umiddelbare opplastinger Sikkerhet + Sti til video-opplasting + Nedlasting av %1$s mappen kunne ikke fullføres diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index a6fc894b..f675c09b 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -190,6 +190,7 @@ Hieronder staan de lokale bestanden en de externe bestanden in %5$s waar ze naar Uw server geeft geen goede userid terug, neem contact op met uw beheerder Kan niet authenticeren tegen deze server + Het account bestaat nog niet in dit apparaat Houd bestand actueel Hernoemen Verwijderen @@ -301,4 +302,6 @@ Hieronder staan de lokale bestanden en de externe bestanden in %5$s waar ze naar Directe uploads Beveiliging Upload Video Pad + Download van %1$s map kon niet worden voltooid + %1$s deelde \"%2$s\" met u diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 5a0b30da..3623617f 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -38,6 +38,8 @@ Poleć znajomemu Wsparcie Stopka + Zapamiętaj położenie udostępnienia + Zapamiętaj ostatnią lokalizację wgrywania Wypróbuj %1$s na swoim smartphonie! Chciałbym zaprosić Cię do używania %1$s na swoim smartfonie!\nŚciągnij tutaj: %2$s Sprawdź serwer @@ -280,6 +282,9 @@ Bezpieczne połączenie jest przekierowywane przez niezabezpieczone trasy. Logi Wyślij historię + Brak aplikacji do wysyłania logów. Zainstaluj klienta poczty! + %1$s Logi aplikacji Android + Ładuję dane... Wymagana autoryzacja Złe hasło Przenieś @@ -292,4 +297,6 @@ aby przenieść ten plik Automatyczne wysyłanie Bezpieczeństwo + Katalog wysyłania dla wideo + Pobieranie %1$s katalogu nie może zostać ukończone diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index 45909b45..baf155f9 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -187,6 +187,7 @@ Seu servidor não está retornando um ID de usuário correto, por favor, entre em contato com um administrador ⇥ Não foi possível autenticar neste servidor + Conta ainda não existe no dispositivo Manter arquivo atualizado Renomear Remover @@ -298,4 +299,6 @@ Envios Instantâneos Segurança Enviar o Caminho do Vídeo + Baixar %1$s da pasta não pode ser completado + %1$s compartilhou \"%2$s\" com você diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml index 60d9377b..6a686c25 100644 --- a/res/values-pt-rPT/strings.xml +++ b/res/values-pt-rPT/strings.xml @@ -41,7 +41,7 @@ Lembrar localização de partilha Lembrar da última localização de envio de partilha Test %1$s no seu smartphone! - Quero convidar-te a usares %1$s no teu smartphone!\nFaz download aqui: %2$s + Eu quero convidar-te para usares %1$s no teu smartphone!\nTransfere aqui: %2$s Verificar Servidor Endereço do servidor https://.. Nome de Utilizador @@ -52,7 +52,7 @@ Enviar Escolha a pasta de envio: A conta não foi encontrada - Não tem nenhuma conta %1$s no seu dispositivo. Por favor, configure primeiro uma conta. + Não existe nenhuma conta %1$s no seu dispositivo. Por favor, configure primeiro uma conta. Configurar Sair Sem conteúdo para enviar @@ -76,7 +76,7 @@ Atualizar ficheiro O ficheiro foi renomeado para %1$s durante o envio. Partilhar a hiperligação - Deixar de partilhar a ligação + Deixar de partilhar a hiperligação Sim Não ACEITAR @@ -98,14 +98,14 @@ Envio bem sucedido %1$s foi enviado com sucesso Não foi possível enviar - O envio do ficheiro %1$s não foi concluído. + Não foi possível concluir o envio de %1$s. Falha no carregamento, é necessário fazer novo login - A transferir ... + A transferir... %1$d%% A transferir %2$s Transferência bem sucedida - %1$s foi descarregado com sucesso - Descarga falhou - O descarregamento %1$s não foi possível descarregar + %1$s foi transferido com sucesso + Transferência falhada + Não foi possível concluir a transferência de %1$s Ainda não foi transferido Não foi possível transferir, tem de iniciar a sessão novamente Escolha a conta @@ -132,10 +132,10 @@ O PIN será pedido sempre que a app seja iniciada. Por favor, reinsira o PIN da App Remover o seu PIN da App - Os códigos PIN introduzidos não são iguais. - Código PIN Incorrecto. - PIN da aplicação removido - PIN da aplicação guardado + Os CÓDIGOS da APP não são iguais + CÓDIGO da App Incorreto + CÓDIGOS da App removido + CÓDIGO da App guardado %1$s leitor de música %1$s (a reproduzir) %1$s (a carregar) @@ -143,8 +143,8 @@ Não foi encontrado nenhum ficheiro de média Não foi fornecida conta O ficheiro não está numa conta válida - Codec de média não suportado - Não foi possível reproduzir o ficheiro + Codec de multimédia não suportado + Não foi possível ler o ficheiro de multimédia Ficheiro erradamente codificado (codec) O tempo de espera para jogar expirou O ficheiro não pode ser reproduzido (streaming) @@ -152,25 +152,25 @@ Erro de segurança a tentar reproduzir o ficheiro %1$s Erro de input a tentar reproduzir %1$s Erro inesperado a tentar reproduzir %1$s - Botão de rebobinar - Botão Tocar/Pausa + Botão de Retroceder + Botão de Reproduzir/Pausar Botão de avanço rápido A obter autorização... - A tentar entrar... + A tentar iniciar a sessão... Sem ligação à rede - Ligação segura indisponível + Ligação segura indisponível. Ligação estabelecida A testar a ligação... Configuração do servidor incorrecta. Uma conta para este utilizador e servidor já existe no dispositivo O utilizador que escreveu não coincide com o nome de utilizador desta conta Ocorreu um erro desconhecido! - Não é possível encontrar o servidor - Instância servidor não encontrada - O servidor levou demasiado tempo a responder + Não foi possível encontrar o anfitrião + Instância do servidor não encontrada + O servidor demorou muito tempo a responder URL errado Inicialização de SSL falhou - Não foi possível verificar a identidade SSL do servidor + Não foi possível verificar a identidade do servidor SSL Versão do servidor não reconhecida Não consegue estabelecer ligação Ligação segura estabelecida @@ -179,13 +179,14 @@ Acesso negado pelo servidor Estado inesperado, por favor, digite a URL do servidor novamente O prazo da sua autorização expirou. Por favor renove-a - Por favor, introduza a password actual + Por favor, insira a palavra-passe atual A sua sessão expirou. Por favor autentique-se de novo A verificar a sua autenticação no servidor... O servidor não suporta este método de autenticação %1$s não suporta contas múltiplas O seu servidor não transmite o ID correcto. Por favor contacte o administrador. Não foi possível autenticar no servidor + Conta ainda não existe no dispositivo manter ficheiro actualizado Renomear Remover @@ -296,4 +297,6 @@ Envios Instantâneos Segurança Envio do Caminho do Vídeo + Não foi possível completar o download da pasta %1$s + %1$s partilhou \"%2$s\" consigo diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml index bb03b024..d87cea3d 100644 --- a/res/values-ro/strings.xml +++ b/res/values-ro/strings.xml @@ -21,10 +21,10 @@ Administrare conturi PIN-ul aplicaţiei Protejaţi-vă clientul - Încărcare instanta de imagine - Încărca instantaneu imagini luate de camera + Încărcare instantă de imagini + Încarcă instantant imagini luate cu camera Încărcare instantă de videoclipuri. - Încarcă videoclipuri instant, filmate cu camera. + Încarcă instant videoclipuri înregistrate cu camera Permite logarea Acesta este folosit pentru a înregistra problemele Istoria logarilor @@ -34,7 +34,10 @@ Recomandati unui prieten Feedback Imprint + Reține contribuie locația + Reține locația fișierului încărcat precedent Încearcă %1$s pe smartphone-ul tău! + Te invit sa folosești %1$s pe smartfonul tău!\nDescarcă aici: %2$s Verificaţi Serverul Adresa serverului https://... Nume utilizator @@ -112,6 +115,7 @@ Conținutul a%1$d fișiere nu a putut fi sincronizat (conflicte %2$d) Unele fisiere locale au fost uitate %1$d fisiere din dosarul %2$s nu a putut fi copiat in + Conform ediției 1.3.16, fișierele încărcate de pe această platformă sunt copiate în dosarul local %1$s pentru a preveni pierderi de date atunci cînd un singur fișier este sincronizat cu mai multe conturi.\n\nDin cauza acestei schimbări, toate fișierele încărcate în edițiile precedente ale acestui app au fost încărcate in dosarul %2$s. Însă acest proces nu fost completat in timpul sincronizării contului din cauza unei erori. Ai opțiunea de a lăsa fișierul intact (fișierele intacte) și de a transfera sursa în dosarul %3$s sau de a schimba locația fișierului(-elor) în dosarul %1$s și de a păstra sursa în %4$s.\n\nMai jos găsești enumerate fișierul local(fișierele locale) și fișierul separat(fișierele separate) în %5$s cu sursa respectivă. Folderul %1$s nu mai există Muta tot/toate Toate fişierele au fost mutate @@ -236,7 +240,7 @@ 12:23:45 Incarca poze doar via WiFi Încarcă videoclipuri doar via WiFi - /Încărcare instanta + /Încărcare instantă Actualizați conflictul Fișierul de la distanță %s nu este sincronizat cu fișierul local. Continuand, se va înlocui conținutul fișierului de pe server. Pastreaza amandoua @@ -245,6 +249,7 @@ Previzualizare imagine Aceasta imagine nu poate fi arătată %1$s nu a putut fi copiat in dosarul local %2$s + Calea de încărcare Ne pare rău, partajarea nu este activată pe server. Vă rugăm să contactați administratorul dvs. A apărut o eroare în timp ce încerca să partajeze acest fișier sau folder A apărut o eroare în timp ce încerca să departajeze sau unshare acest fișier sau folder @@ -267,11 +272,23 @@ Fișierul nu mai este disponibil pe server Conturi Adaugă cont + Conexiunea securizată este redirecționată către un traseu neasigurat. + Înregistrări + Trimite Istoria + App-ul de trimitere a inregistrărilor nu a fost găsit. Instalează mail app-ul! + %1$s înregistrările app-ului Android + Datele se încarcă... Autentificare necesară Parolă greșită Mutare Nu este nimic aici. Poți adăuga un director! Alege + Incapabil de trasfer. Verifică existența fișierului + Fișierul există deja în dosarul de destinație + O eroare apare la transferarea acestui fișier sau dosar pentru a muta acest fișier + Încărcări instante Securitate + Calea de încărcare Video + Descărcarea fișierului %1$s nu s-a finisat diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 20415883..cc739418 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -1,13 +1,13 @@ - %1$s Приложение для Андроида - Версия %1$s + %1$s для Android + версия %1$s Обновить учетную запись Загрузить Содержимое из других приложений Файлы Открыть с помощью - Новая папка + Новый каталог Настройки Подробно Отправить @@ -15,7 +15,7 @@ Упорядочить по А-Я - Новые - Старые + Новое - Старое @@ -23,12 +23,12 @@ Больше Учётные записи Управление учётными записями - App PIN + PIN приложения Защитить ваш клиент - Быстрая загрузка фотографий + Мгновенная загрузка фотографий Немедленно загружать фотографии сделанные камерой - Быстрая загрузка видео - Быстрая загрузка видео с камеры + Мгновенная загрузка видео + Немедленно загружать видео, сделанные камерой Включить журналирование Используется для регистрации ошибок Журнал @@ -38,7 +38,7 @@ Рекомендовать другу Обратная связь Штамп - Запомнить расположение публикации + Запомнить расположение общего ресурса Запомнить расположение загрузки последней публикации Попробуйте %1$s на вашем смартфоне! Хочу предложить вам использовать %1$s на смартфоне!\nЗагрузить можно здесь: %2$s @@ -51,21 +51,21 @@ Файлы Подключиться Загрузить - Выберете папку для загрузки + Выберите каталог для загрузки Учётная запись не найдена - На вашем устройстве нет учётных записей %1$s. Сначала нужно настроить учётную запись. - Установка + На вашем устройстве нет учётных записей %1$s. Пожалуйста, настройте учётную запись. + Настройка Выход Нет содержимого для загрузки Содержимое не получено. Нечего загружать. - %1$s не имеет доступа к опубликованным данным + Доступ к общему ресурсу для %1$s запрещен Загрузка - только что + пару секунд назад Здесь ничего нет. Загрузите что-нибудь! Загрузка... - В данной папке нет файлов. - папка - папки + В этом каталоге нет файлов. + каталог + каталоги файл файлы Нажмите на файл для отображения дополнительной информации. @@ -77,7 +77,7 @@ Обновить файл Файл был переименован в %1$s во время загрузки Поделиться ссылкой - Удалить ссылку + Убрать ссылку Да Нет ОК @@ -86,68 +86,68 @@ Отмена Сохранить и выйти Ошибка - Идёт загрузка... + Загрузка ... Неизвестная ошибка О программе Сменить пароль Удалить учётную запись Создать учётную запись - Загрузить из... - Имя папки - Загрузка... - %1$d%% загрузки %2$s + Загрузить из ... + Имя каталога + Загрузка ... + %1$d%% Загружается %2$s Загрузка завершена %1$s был успешно загружен Ошибка загрузки Загрузка %1$s не может быть завершена - Загрузка не удалась, Вам необходимо переподключиться - Скачивание... - %1$d%% скачивания %2$s + Загрузка не удалась, нужно заново войти в свою учетную запись + Скачивание ... + %1$d%% Скачивается %2$s Скачивание завершено %1$s успешно скачан Скачивание не удалось Скачивание %1$s не может быть завершено Ещё не скачано - Скачивание не удалось, Вам необходимо переподключиться + Скачивание не удалось, нужно заново войти в свою учетную запись Выберите учётную запись Синхронизация прошла неудачно - Синхронизация не удалась, Вам необходимо переподключиться + Синхронизация не удалась, нужно заново войти в свою учетную запись Синхронизация %1$s не может быть завершена Неверный пароль для %1$s Обнаружены конфликты - %1$d файлы не могут быть синхронизированы + %1$d файлов не может быть синхронизировано Не удалось синхронизировать файлы Содержимое %1$d файлов не может быть синхронизировано (конфликтов: %2$d) - Несколько локальных файлов были забыты - Не возможно скопировать %1$d файлы из %2$s папки - Начиная с версии 1.3.16, файлы, загружаемые с этого устройства, копируются в локальную директорию %1$s, чтобы предотвратить потерю данных при синхронизации файла с несколькими учётными записями.\n\nПоэтому все файлы, загруженные предыдущими версиями данного приложения, были скопированы в директорию %2$s. Однако, во время синхронизации что-то помешало завершить эту операцию. Теперь можно либо оставить файлы как есть и удалить ссылку на %3$s, либо переместить их в %1$s и сохранить ссылку на %4$s.\n\nНиже перечислены локальные файлы, и соответствующие им удалённые файлы в %5$s, к которым они привязаны. + Некоторые загруженные файлы не были перенесены в локальную папку + Невозможно скопировать %1$d файлов из папки %2$s + Начиная с версии 1.3.16, файлы, загружаемые с этого устройства, копируются в локальный каталог %1$s, чтобы предотвратить потерю данных при синхронизации файла с несколькими учётными записями.\n\nПоэтому все файлы, загруженные предыдущими версиями данного приложения, были скопированы в каталог %2$s. Однако, во время синхронизации что-то помешало завершить эту операцию. Можете оставить файлы как есть и удалить ссылку на %3$s, либо переместить их в %1$s и сохранить ссылку на %4$s.\n\nНиже перечислены локальные файлы, и соответствующие им удалённые файлы в %5$s, к которым они привязаны. Каталог %1$s больше не существует Переместить всё Все файлы были перемещены Некоторые файлы не могут быть перемещены - Локально: %1$s - Удаленно: %1$s - Для копирования выбранных файлов в папку %1$s недостаточно свободного места. Скопировать в другое место? - Вставьте App PIN - Введите App PIN - PIN-код будет запрашиваться при каждом запуске приложения. - Повторите ввод App PIN - Удалить App PIN - Введённые App PIN не совпадают - Неверный App PIN - App PIN удалён - App PIN сохранён + Локальные: %1$s + Удаленные: %1$s + Для копирования выбранных файлов в каталог %1$s недостаточно свободного места. Скопировать в другое место? + Укажите PIN приложения + Введите PIN приложения + PIN будет запрашиваться при каждом запуске приложения. + Повторите ввод PIN приложения + Удалить PIN приложения + Введённые PIN не совпадают + Неверный PIN приложения + PIN приложения удалён + PIN приложения сохранён %1$s аудиоплеер %1$s (проигрывается) %1$s (загружается) %1$s воспроизведение завершено - Медиафайлов не найдено - Учётная запись не настроена + Медиафайлы не найдены + Учётная запись не указана Файл в неверной учётной записи Неподдерживаемый кодек Медиафайл не может быть прочитан Медиафайл некорректно закодирован - Время попыток воспроизведения вышло + Истекло время попытки воспроизведения Невозможно организовать потоковую передачу медиафайла Медиафайл не может быть проигран стандартным плеером Ошибка безопасности при воспроизведении %1$s @@ -156,7 +156,7 @@ Перемотка назад Воспроизведение или пауза Перемотка вперед - Происходит авторизация..... + Выполняется авторизация... Попытка входа... Нет подключения к сети Защищённое соединение недоступно. @@ -171,23 +171,24 @@ Сервер слишком долго не отвечает Неверный URL Ошибка инициализации SSL - Невозможно проверить SSL-сертификат сервера + Невозможно проверить SSL подлинность сервера Неизвестная версия сервера - Невозможно установить соединение + Не удается установить соединение Защищённое соединение установлено Неверное имя пользователя или пароль Ошибка авторизации Сервер авторизации отказал в доступе Неожиданный ответ; введите адрес сервера ещё раз Время авторизации истекло. Пожалуйста, авторизуйтесь снова - Пожалуйста, введите пароль + Пожалуйста, укажите текущий пароль Время сессии истекло. Пожалуйста, подключитесь снова Подключение к серверу аутентификации... Сервер не поддерживает выбранный метод аутентификации - %1$s не поддерживает сразу несколько учётных записей - Ваш сервер не возвращает корректный пользовательский идентификатор, пожалуйста свяжитесь с администратором + %1$s не поддерживает несколько учётных записей + Сервер вернул некорректный пользовательский идентификатор. Пожалуйста, свяжитесь с вашим администратором ⇥ - Невозможно аутентифицироваться на этом сервере + Невозможно авторизоваться на этом сервере + Аккаунт не существует на устройстве ещё Обновлять файл Переименовать Удалить @@ -204,13 +205,13 @@ Переименование не может быть завершено Удаленный файл не может быть проверен Содержимое файла уже синхронизировано - Не возможно создать папку + Невозможно создать каталог Недопустимые символы: / \\ < > : \" | ? * Имя файла не может быть пустым Подождите немного Неизвестная ошибка; выберите этот файл из другого приложения Файлы не выбраны - Отправить ссылку... + Отправить ссылку ... Войти через oAuth2 Подключение к серверу oAuth2... Подлинность сайта не может быть проверена @@ -218,81 +219,87 @@ - Срок действия сертификата сервера истёк - Срок действия сертификата сервера ещё не начался - URL не совпадает с именем сервера в сертификате - Вы хотите доверять данному сертификату в любом случае? + Доверять этому сертификату в любом случае? Сертификат не может быть сохранён Подробно Скрыть - Кому выдано: - Кем выдано: + Кому выдан: + Кем выдан: Имя: Организация: - Организационное подразделение: + Подразделение: Страна: Штат: Местонахождение: Срок действия: - Из: - В: + С: + По: Подпись: Алгоритм: Сертификат не может быть показан. - - Информации об ошибке нет + - Нет информации об ошибке Это заполнитель placeholder.txt Изображение PNG 389 КБ 2012/05/18 12:23 PM 12:23:45 - Загружать изображения только через Wi-Fi + Загрузка изображений только через Wi-Fi Загрузка видео только через WiFi /InstantUpload Конфликт обновления Удаленный файл %s не синхронизирован с локальным. Продолжение приведет к замене содержимого файла на сервере. Сохранить оба - Заменить + Перезаписать Не загружать Предпросмотр Это изображение не может быть отображено - %1$s не возможно скопировать в локальною папку %2$s + %1$s невозможно скопировать в локальный каталог %2$s Путь для загрузки - К сожалению, на вашем сервере отключен совместный доступ. Пожалуйста, свяжитесь с вашим администратором. - Невозможно добавить в общий доступ. Пожалуйста, проверьте, существует ли файл - Ошибка предоставления общего доступа к этому файлу или каталогу - Невозможно убрать из общего доступа. Пожалуйста, проверьте, существует ли файл - Ошибка удаления общего доступа к этому файлу или каталогу + Механизм общего доступа не включен на данном сервере. Пожалуйста, свяжитесь с вашим +⇥⇥администратором. + Невозможно поделиться. Убедитесь, что файл существует + При попытке поделиться этим файлом или каталогом произошла ошибка + Невозможно закрыть доступ. Убедитесь что файл существует + При попытке закрыть доступ к этому файлу или каталогу произошла ошибка Отправить Копировать ссылку Скопировано в буфер обмена - Критическая ошибка: невозможно выполнить операции + Критическая ошибка: невозможно выполнить действия При подключении к серверу возникла ошибка - Во время ожидания сервера возникла ошибка, операция не может быть завершена - Во время ожидания сервера возникла ошибка, операция не может быть завершена - Операция не может быть завершена, сервер недоступен + Во время ожидания сервера произошла ошибка, действие не может быть выполнено + Во время ожидания сервера произошла ошибка, действие не может быть выполнено + Действие не может быть выполнено, сервер недоступен - У вас нет доступа %s - переименовать этот файл - удалить этот файл - опубликовать этот файл - отменить публикацию этого файла - создать файл - загрузить в эту папку + У вас нет прав %s + для переименования этого файла + для удаления этого файла + для открытия доступа к этому файлу + для закрытия доступа к этому файлу + для создания файла + для загрузки в этот каталог Этот файл больше недоступен на сервере Учётные записи Добавить учетную запись - Защищённое соединение перенаправлено по незащищённому маршруту + Защищённое соединение перенаправлено по небезопасному маршруту Журналы История Отправлений + Приложение для отправки журнала не найдено. Установите почтовое приложение! + Журналы %1$s для Android + Загрузка данных… Требуется аутентификация Неправильный пароль Переместить - Здесь ничего нет. Вы можете добавить папку! + Здесь ничего нет. Вы можете добавить каталог! Выбрать - Невозможно переместить. Пожалуйста, проверьте, существует ли файл - Невозможно переместить папку в папку-потомок - Файл уже существует в папке назначения - Произошла ошибка при попытке перемещения этого файла или папки - переместить этот файл + Невозможно переместить. Убедитесь, что файл существует + Невозможно переместить каталог в его подкаталог + Файл уже существует в каталоге назначения + Произошла ошибка при попытке перемещения этого файла или каталога + для перемещения этого файла Мгновенные загрузки Безопасность Путь для загрузки Видео + Загрузка папки %1$s не может быть завершена + %1$s предоставил вам доступ к \"%2$s\" diff --git a/res/values-sk-rSK/strings.xml b/res/values-sk-rSK/strings.xml index 1566dede..9a70c227 100644 --- a/res/values-sk-rSK/strings.xml +++ b/res/values-sk-rSK/strings.xml @@ -187,6 +187,7 @@ Váš server nevracia správne používateľské id, kontaktujte prosím správcu systému Nie je možné vykonať autentifikáciu na server + Účet zatiaľ v zariadení neexistuje Udržiavať súbor aktuálny. Premenuj Odober @@ -282,6 +283,9 @@ Zabezpečené pripojenie je presmerované na nezabezpečenú trasu. Logy Odoslať históriu + Nebola nájdená aplikácia pre odosielanie log protokolov. Nainštalujte si mailovú aplikáciu! + %1$s Android app logs + Načítavam dáta... Vyžaduje sa overenie Nesprávne heslo Presunúť @@ -294,4 +298,7 @@ pre presun tohoto súboru Okamžité nahratie Zabezpečenie + Cesta pre nahrávanie videí + Sťahovanie %1$s priečinka nebolo dokončené + %1$s vám zdieľa „%2$s“ diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index 731bd67f..3f99fb3b 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -298,4 +298,6 @@ Takojšnje pošiljanje v oblak Varnost Pot videa za pošiljanje + Imenika %1$s ni mogoče prejeti v celoti + Uporabnik %1$s je omogočil souporabo \"%2$s\". diff --git a/res/values-sr-rSP/strings.xml b/res/values-sr-rSP/strings.xml index b5d14f16..41e12381 100644 --- a/res/values-sr-rSP/strings.xml +++ b/res/values-sr-rSP/strings.xml @@ -2,6 +2,7 @@ Pošalji Fajlovi + Novi direktorijum Podešavanja Detaljnije Pošalji @@ -22,14 +23,18 @@ Veličina: Tip: Preuzmi + Podeli prečicu Da Ne Ok + Otkaži otpremanje Otkaži Greška + Nepoznata greška Izmeni lozinku Ukloni nalog Novi nalog + Ime fascikle Otpremanje... Uspešno otpremljeno Otpremanje nije uspelo diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index b9235ad7..3311ba24 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -1,116 +1,293 @@ + %1$s Андроид апликација + верзија %1$s + Освежи налог Отпреми - Садржај са других апликација - Датотеке + Садржај из других апликација + Фајлови + Отвори помоћу + Нова фасцикла Поставке + Детаљи Пошаљи + Разврстај + Разврставање + + A-Z + новији - старији + Опште Више Налози + Управљање налозима + ПИБ апликације + Заштитите програм + Тренутно отпремање фотографија + Тренутно отпремај фотографије сликане камером + Тренутно отпремање видеа + Тренутно отпремај видео снимљен камером + Укључи бележење + Ово се користи за бележење проблема + Историјат бележења + Ово приказује сачуване записнике + Обриши историјат Помоћ + Препоручи пријатељу + Ваше мишљење + Жиг + Упамти локацију дељења + Памти последњу локацију отпремања дељења + Пробајте %1$s на вашем телефону! + Предлажем вам да пробате %1$s на вашем телефону!\nПреузмите овде: %2$s + Провери сервер + Адреса сервера https://… Корисничко име Лозинка + Нов вам је %1$s? Фајлови - Повежи ме + Повежи се Отпреми + Изаберите фасциклу отпремања: Нема налога + Нема %1$s налога на вашем уређају. Прво подесите налог. Подеси - Изађи + Напусти Нема садржаја за отпремање - Садржај није примљен. Нема ништа да се отпреми. + Садржај није примљен. Нема шта да се отпреми. Отпремање - пре неколико секунди + пре пар секунди Овде нема ничег. Отпремите нешто! - Додирните датотеку ради приказа додатних информација. + Учитавам + Нема фајлова у овој фасцикли. + Тапните на фајл ради приказа додатних информација. Величина: Врста: - Направљено: - Измењено: + Направљен: + Измењен: Преузми - Освежи датотеку + Освежи фајл + Фајл је преименован у %1$s током отпремања + Веза дељења + Не дели везом Да Не У реду - Обустави преузимање - Прекини слање + Откажи преузимање + Откажи отпремање Откажи Сачувај и изађи Грешка + Учитавам... + Непозната грешка О програму Измени лозинку Обриши налог Отвори налог Отпреми из… + Назив фасцикле Отпремам… %1$d%% Отпремам %2$s Отпремање је успело + %1$s је успешно отпремљен Отпремање није успело - Не могу да довршим отпремање датотеке %1$s + Не могу да довршим отпремање %1$s + Отпремање неуспешно. Поново се пријавите. Преузимам… %1$d%% Преузимам %2$s Преузимање успешно %1$s је успешно преузет Преузимање није успело - Не могу да довршим преузимање датотеке %1$s + Не могу да довршим преузимање %1$s Још увек није преузето - Изабери налог + Преузимање неуспешно. Пријавите се поново + Изаберите налог Синхронизовање није успело - Не могу да довршим синхронизацију датотеке %1$s - Све датотеке су померене - Неке датотеке нису могле бити померене - Унесите PIN апликације - Са сваким покретањем апликације мораћете да унесете PIN + Синхронизовање неуспешно. Пријавите се поново + Не могу да довршим синхронизацију %1$s + Неисправна лозинка за %1$s + Постоје сукоби + Од верзије 1.3.16, фајлови отпремљени са уређаја се копирају у локалну фасциклу %1$s да би се спречио губитак података када се исти фајл синхронизује са више налога.\n\nЗбог ове измене, сви фајлови отпремљени са претходним верзијама ове апликације се копирају у фасциклу %2$s. Међутим, грешка је онемогућила довршавање ове радње током синхронизације налога. Можете или оставити фајлове како јесу и уклонити линк до %3$s или преместити фајлове у фасциклу %1$s и задржати везу до %4$s.\n\nИспод су наведени локални фајлови и удаљени фајлови у %5$s са којима су повезани. + Фасцикла %1$s више не постоји + Премести све + Сви фајлови су премештени + Неки фајлови нису могли бити премештени + Локално: %1$s + Удаљено: %1$s + Нема довољно простора да би се изабрани фајлови копирали у фасциклу %1$s. Желите ли да их преместите? + Унесите ПИБ апликације + Унесите ПИБ за апликацију + Са сваким покретањем апликације мораћете да унесете ПИБ + Унесите ПИБ поново + Уклоните ПИБ апликације + Бројеви се не поклапају + Неисправан ПИБ + ПИБ је уклоњен + ПИБ је упамћен + %1$s музички плејер + %1$s (пуштам) + %1$s (учитавам) + %1$s пуштање завршено + Нема медијских фајлова + Није наведен налог + Фајл није у исправном налогу + Неподржан кодек + Медијски фајл се не може читати + Медијски фајл није исправно кодиран + Време истекло у покушавању пуштања + Медијски фајл се не може пустити + Медијски фајл се не може пустити са фабричким плејером + Безбедносна грешка при покушају пуштања %1$s + Улазна грешка при покушају пуштања %1$s + Неочекивана грешка при покушају пуштања %1$s + Уназад + Пуштање-пауза + Унапред + Тражим ауторизацију... + Покушавам пријављивање... Нема мрежне везе Безбедна веза није доступна. Веза је успостављена - Дошло је до непознате грешке. + Тестирам везу... + Лоше подешавање сервера + Налог са истим корисником и сервером већ постоји на уређају + Унесени корисник се не поклапа са корисником овог налога + Дошло је до непознате грешке! Не могу да пронађем домаћина Не могу да пронађем примерак сервера Серверу је требало предуго да се одазове Погрешно уобличена адреса - Покретање SSL-а није успело + ССЛ иницијализација није успела + Не могу да проверим ССЛ идентитет сервера + Непозната верзија сервера Не могу да успоставим везу Безбедна веза је успостављена - Редовно ажурирај датотеку + Погрешно име или лозинка + Неуспешна ауторизација + Сервер ауторизације је одбио приступ + Неочекивано стање. Унесите поново адресу сервера + Ауторизација је истекла. Урадите је поново + Унесите тренутну лозинку + Сесија је истекла. Повежите се поново + Повезујем се на сервер аутентификације... + Сервер не подржава овај начин аутентификације + %1$s не подржава вишеструке налоге + Сервер не враћа исправан ИД корисника. Контактирајте администратора + + Не могу да аутентификујем са овим сервером + Не постоји налог на уређају + Редовно ажурирај фајл Преименуј Уклони + Желите да уклоните %1$s? + Желите да уклоните %1$s и њен садржај? Само локално + Само локални садржај Уклони са сервера Удаљено и локално - Унесите ново име + Уклањање успешно + Уклањање неуспешно + Унесите нов назив + Локална копија се не може преименовати. Покушајте други назив Не могу да довршим преименовање - Удаљена датотека се не може проверити + Удаљени фајл се не може проверити + Садржај је већ синхронизован + Фасцикла се не може направити + Забрањени знакови: / \\ < > : \" | ? * + Назив фајла не може бити празан Сачекајте тренутак - Нисте изабрали датотеку + Неочекивани проблем. Изаберите фајл другом апликацијом + Нисте изабрали фајл + Пошаљи везу ... + Пријави се помоћу „oAuth2“ + Повезујем се на „oAuth2“ сервер... Не могу да проверим идентитет сајта - – Сертификат сервера није поверљив + – Сертификат сервера није од поверења – Сертификат сервера је истекао + - Датуми важења сертификата су у будућности – Адреса се не поклапа са именом домаћина у сертификату - Желите ли ипак да означите сертификат као поверљив? + Желите ли ипак да верујете сертификату? Не могу да сачувам сертификат Подаци Сакриј Издато за: - Издао/ла: + Издавач: Уобичајено име: Организација: Организациона јединица: - Земља: - Држава: + Држава: + Покрајина: Локација: Ваљаност: Од: За: Потпис: Алгоритам: + Сертификат се не може приказати. + - Нема података о грешци + Ово је местодржач + чувамместо.txt + ПНГ слика + 389 KB + 2012/05/18 12:23 ПоП + 12:23:45 Отпремај слике само путем бежичне мреже + Отпремај видео само путем бежичне мреже Ажурирај сукоб + Удаљени фајл %s није синхронизован са локалним. Ако наставите, заменићете фајл на серверу. + Задржи оба + Пребриши + Не отпремај + Преглед слике + Слика се не може приказати + %1$s се не може копирати у локалну фасциклу %2$s + Путања отпремања + Дељење није укључено на вашем серверу. Контактирајте + администратора. + Не могу да делим. Проверите да ли фајл постоји + Дошло је до грешке приликом покушаја дељења овог фајла или фасцикле + Не могу да прекинем дељење. Проверите да ли фајл постоји + Дошло је до грешке приликом покушаја укидања дељења овог фајла или фасцикле Пошаљи + Копирај везу + Копирано у клипборд + Критична грешка: не могу да радим + Дошло је до грешке при повезивању са сервером. + Дошло је до грешке при чекању на сервер. Радња није могла бити урађена + Дошло је до грешке при чекању на сервер. Радња није могла бити урађена + Радња није могла бити довршена. Сервер је недоступан + Немате дозволу %s + да преименујете овај фајл + да обришете овај фајл + да делите овај фајл + да укинете дељење овог фајла + да направите фајл + да отпремате у ову фасциклу + Фајл није више доступан на серверу Налози + Додај налог + Безбедна веза је преусмерена на небезбедну руту + Записници + Историјат слања + Нема начина за слање записника. Инсталирајте апликацију е-поште! + Записници %1$s Андроид апликације + Учитавам податке... + Неопходна провера идентитета + Погрешна лозинка + Премести + Овде нема ничега. Можете додати фасциклу! Одабери + Не могу да преместим. Проверите да ли фајл постоји + Није могуће преместити фасциклу у њену потфасциклу + Фајл већ постоји у одредишној фасцикли + Дошло је до грешке при премештању фајла или фасцикле + да преместите овај фајл + Тренутна отпремања Безбедност + Путања отпремања видеа + Преузимање фасцикле %1$s не може бити довршено + %1$s подели „%2$s“ са вама diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index f87edff8..f44afacc 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -11,6 +11,12 @@ Inställningar Detaljer Skicka + Sortera + Sortera efter + + A-Ö + Nyast - Äldst + Allmänt @@ -32,6 +38,8 @@ Rekommendera till en vän Feedback Imprint + Kom ihåg plats för delat + Kom ihåg senaste uppladdningsplats vid dela Prova %1$s på din smartphone! Jag skullje vilja bjuda in dig till att prova %1$s på din smartphone!\nLadda ner appen från Google Play här: %2$s Kontrollera Server @@ -111,6 +119,7 @@ Innehållet i %1$d filer kunde inte synkas (%2$d konflikter) Vissa lokala filer glömdes %1$d filer från %2$s mappar kunde inte kopieras till + Från och med version 1.3.16 kommer filer uppladdade från denna enhet kopieras in till lokal %1$s mapp för att förhindra dataförlust när en enskild fil synkroniseras med flera konton.\n\nPå grund av denna ändring kommer alla filer uppladdade i tidigare versioner av denna applikation kopieras in till %2$s mapp. Dock förhindrade ett fel slutförandet av denna operation under konto-synkronisering. Du kan antingen lämna filerna som de är och ta bort länken till %3$s, eller flytta filerna in till %1$s mapp och behålla länken till %4$s.\n\nNedan listas de lokala filerna och de fjrran filerna i %5$s som de länkades till. Mappen %1$s existerar inte längre Flytta allt Alla filer flyttades @@ -244,9 +253,12 @@ Förhandsvisa bild Denna bild kan inte visas %1$s kunde inte kopieras till %2$s lokal mapp + Uppladdnings-sökväg Ledsen, delning är inte aktiverat på din server. Vänligen kontakta din administratör. + Lyckades ej dela. Vänligen kontrollera om filen eisterar Ett fel uppstod vid försök att dela denna fil eller mapp + Lyckades ej sluta dela. Vänligen kontrollera om filen existerar Ett fel uppstod vid försök att sluta dela denna fil eller mapp Skicka Kopiera länk @@ -267,12 +279,23 @@ Filen är inte längre tillgänglig på servern Konton Lägg till konto + Säker anslutning är omdirigerad till en osäker väg. + Loggar + Skickat historik + Ingen app för att skicka loggar hittades. Installera mail appen! + %1$s Android app logs + Laddar data... Autentisering krävs Fel lösenord Flytta Ingenting här. Du kan skapa en mapp! Välj Gick inte att flytta. Vänligen kontrollera att filen existerar + Det är inte möjligt att flytta mappen in i underliggande struktur + Filen existerar redan i destinationsmappen + Ett fel uppstod vid försök att flytta denna fil eller mapp att flytta den här filen + Direktuppladning Säkerhet + Uppladdnings-sökväg för video diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml index 680190c8..8805fdce 100644 --- a/res/values-tr/strings.xml +++ b/res/values-tr/strings.xml @@ -64,9 +64,9 @@ Yükleniyor... Bu klasörde dosya yok. klasör - klasörler + klasör dosya - dosyalar + dosya Ek bilgileri görmek için dosyaya dokunun. Boyut: Tür: @@ -187,6 +187,7 @@ Sunucunuz geçerli bir kullanıcı kimliği döndürmüyor, lütfen yöneticinizle iletişime geçin Bu sunucuya karşı kimlik doğrulama yapılamaz + Hesap henüz cihazda mevcut değil Dosyayı güncel tut Yeniden adlandır Kaldır @@ -282,9 +283,9 @@ Güvenli bağlantı, güvenli olmayan bir rotaya yönlendirildi. Günlükler Geçmişi Gönder - Logları göndermek için uygulama bulunamadı. Eposta uygulamasını yükleyin! + Kayıtları göndermek için uygulama bulunamadı. E-posta uygulamasını yükleyin! %1$s Android uygulama kayıtları - Yükleniyor... + Veri yükleniyor... Kimlik doğrulama gerekli Hatalı parola Taşı @@ -298,4 +299,6 @@ Anında Yüklemeler Güvenlik Video Yükleme Yolu + %1$s klasörün indirilmesi tamamlanamadı + %1$s sizinle \"%2$s\" paylaşımını yaptı diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index 4c07d79b..64c57d30 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -38,6 +38,8 @@ Порадити товаришу Зворотній зв\'язок Відбиток + Запам\'ятати позицію + Запам\'ятати останній опублікований шлях завантаження Спробуйте %1$s на своєму смартфоні! Пропоную вам користуватися %1$s на вашому смартфоні!\nЗавантажити можна за посиланням: %2$s Перевірити сервер @@ -98,7 +100,7 @@ Помилка завантаження Завантаження %1$s не може завершитись Завантажити не вдалося, необхідно повторити вхід - Зкачування … + Скачування … %1$d%% Зкачування %2$s Успішно зкачано %1$s успішно завантажено @@ -279,6 +281,9 @@ Безпечне підключення перенаправляється через незабезпечений маршрут. Журнали Надіслати історію + Немає додатку для відправки журналів. Встановіть поштовий додаток! + %1$s Android лог додатку + Завантаження даних... Потрібна аутентифікація Невірний пароль Перемістити @@ -291,4 +296,6 @@ перемістити цей файл Миттєво завантаження Безпека + Шлях завантаження відео + Скачування теки %1$s не може бути завершено diff --git a/res/values-yo/strings.xml b/res/values-yo/strings.xml new file mode 100644 index 00000000..69623e19 --- /dev/null +++ b/res/values-yo/strings.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index e8852f5e..28123e0a 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -4,13 +4,19 @@ 版本:%1$s 刷新帐户 上传 - 来自其它app的内容 + 来自其它应用的内容 文件 - 打开 - 增加文件夹 + 打开方式 + 新建文件夹 设置 详细信息 发送 + 排序 + 排序方式 + + A - Z + 新 - 旧 + 常规 @@ -18,13 +24,13 @@ 账号 管理账号 App PIN - 保护您的App客户端 + 保护客户端 即时图片上传 即时上传相机拍摄的图片 - 立即上传视频 + 即时上传视频 即时上传由相机拍摄的视频 开启日志 - 这过去是日志问题 + 用于记录问题 日志历史 这显示已经保存的日志 删除历史 @@ -32,27 +38,28 @@ 推荐给朋友 反馈 版本说明 - 在您的智能手机上试用一下 %1$s! - “我邀请你使用在你的智能手机上使用 %1$s,在这下载:%2$s” - + 记住共享位置 + 记住上次共享上传的位置 + 在您的智能手机上试用 %1$s! + 我邀请你在智能手机上使用 %1$s\n下载路径:%2$s 检查服务器 服务器地址 https://... 用户名 密码 - 新增到 %1$s? + 初次使用 %1$s? 文件 连接 上传 选择上传文件夹: 未找到账号 - 设备上未找到账号,请先创建账号。 + 设备上未找到 %1$s 账号,请先设置账号。 设置 退出 - 没有上传的内容 - 没有接收到内容,无可上传。 + 没有需要上传的内容 + 没有接收到内容,没有需要上传的内容。 %1$s未被允许访问共享内容。 上传 - 秒前 + 几秒前 这里还什么都没有。上传些东西吧! 载入中.... 在该文件夹中不存在文件。 @@ -60,19 +67,19 @@ 文件夹 文件 文件 - 点击一个文件来显示额外的信息。 + 点击一个文件可以显示额外的信息。 大小: 类型: 创建于: - 已修改: + 修改于: 下载 刷新文件 上传过程中文件被更名为了 %1$s 分享链接 - 取消共享链接 + 取消分享链接 是 否 - OK + 确定 取消下载 取消上传 取消 @@ -85,35 +92,35 @@ 删除账号 创建账号 上传自... - 目录名称 + 文件夹名称 上传... %1$d%% 上传 %2$s 上传成功 %1$s 成功上传 上传失败 - 1$上传未能完成 + %1$s 未能成功上传 上传失败,您需要重新登录 - 下载中…… + 下载中... %1$d%% 下载中 %2$s 下载成功 - %1$s 成功下载 + 成功下载 %1$s 下载失败 - 下载1$s 未能完成 + %1$s 下载未能完成 未下载完毕 下载失败,您需要重新登录 选择账户 同步失败 同步失败,您需要重新登录 %1$s同步未完成。 - 密码错误%1$s + %1$s 的密码错误 发现冲突 %1$d 文件无法同步 文件同步失败 - 无法同步 %1$d 文件内容(与 %2$d 冲突) + 无法同步 %1$d 文件内容(%2$d 冲突) 某些本地文件已被遗忘 %2$s 目录中的 %1$d 个文件不能被复制到 从 1.3.16 版起,从此设备上传的文件将被复制到本地的 %1$s 文件夹,以防止某个单一文件在多个账户间同步而造成的数据损失。\n\n 由于此项变化,此应用之前的版本上传的全部文件都已被复制到了 %2$s 文件夹。然而,账户同步期间有一个错误阻止了此操作的完成。您可能想保持文件不动,并移除指向 %3$s 的链接,或将文件移动到 %1$s 文件夹中并保持其到 %4$s 的链接。下面列出的是本地文件,以及它们被链接到的 %5$s 中的远程文件。 - 文件夹%1$s 不存在 + 文件夹%1$s 已经不存在 移动所有 所有文件已被移动 某些文件无法被移动 @@ -128,7 +135,7 @@ 两次 App PIN码不同 App PIN码不正确 App PIN码已移除 - App PIN码已保存。 + App PIN码已保存 %1$s 音乐播放器 %1$s (播放中) %1$s (载入中) @@ -149,16 +156,16 @@ 播放暂停按钮 快进按钮 正在认证... - 尝试登录 + 尝试登录... 没有网络连接 - 安全链接无效。 + 安全连接不可用。 连接已建立。 测试连接…… - 服务器配置不正确。 + 服务器配置不正确 此设备中已经存在同名同服务器的帐号 输入用户与此帐户的用户不符 发生未知错误! - 无法找到服务器 + 无法找到主机 未发现服务器实例 看起来服务器不太给力 网址不正确 @@ -166,13 +173,13 @@ 无法验证 SSL 服务器的身份 不可辨识的服务器服务器版本 无法建立连接 - 加密连接已建立 - 用户名或密码错误! + 安全连接已建立 + 用户名或密码错误 认证不成功 访问被认证服务器拒绝 意外状态;请再次输入服务器的地址 你的授权已经过期。请重新授权。 - 请输入当前密码: + 请输入当前密码 您的会话超时了,请重新连接 正在连接到认证服务器.... 服务器不支持这种验证方式 @@ -201,15 +208,15 @@ 文件名不能为空 请稍候 未知问题;请试试用其他程序选择此文件 - 未选择文件。 + 未选择文件 发送链接给 … 使用oAuth2登陆 连接oAuth2 服务器... 站点身份无法验证 - 不受信任的服务器证书 - 服务器证书过期 - 服务器证书过新 - 主机名与证书中的记录不匹配 + - 不受信任的服务器证书 + - 服务器证书过期 + - 服务器证书时间比当前时间还晚 + - 主机名与证书中的记录不匹配 是否信任此证书? 证书无法保存 详细信息 @@ -235,8 +242,8 @@ 389字节 2012/05/18 下午12:23 12:23:45 - 仅通过WIFI上传图片。 - 仅在 WIFI 下上传视频 + 仅通过 WIFI 上传图片。 + 仅通过 WIFI 上传视频 /InstantUpload 上传冲突 远程文件 %s 未与本地文件同步。继续将替换服务器上的文件内容。 @@ -244,8 +251,9 @@ 覆盖 不上传 图片预览 - 不能显示图片 + 无法显示图片 无法复制 %1$s 到本地目录 %2$s + 上传路径 抱歉,共享功能未启用。请联系管理员。 无法共享。请检查文件是否存在 共享文件或目录出错 @@ -260,27 +268,34 @@ 等待服务器响应时发生了一个错误,此操作无法完成 服务器不可用,此操作无法完成 - 你没有许可%s + 你没有权限%s 重命名该文件 删除该文件 - 分享该文件 + 共享该文件 取消共享该文件 创建文件 - 上传此文件夹 + 在此文件夹上传 该文件在服务器上不可用 账号 添加账号 + 安全连接被重定向到非安全路径. 日志 发送历史 + 未找到可以发送日志的程序。请安装 mail! + %1$s Android 程序日志 + 载入数据... 需要认证 错误密码 移动 这里还什么都没有。上传些东西吧! - 选择(&C)... + 选择 无法移动。请检查文件是否存在 - b不能够把一个目录移动到它的下级 + 无法把一个目录移动到它的下级 该文件已经存在在目标文件夹 尝试移动该文件或文件夹时发生错误 移动该文件 + 即时上传 安全 + 视频上传路径 + %1$s 文件夹的下载无法完成 diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml index b2731798..892944eb 100644 --- a/res/values-zh-rHK/strings.xml +++ b/res/values-zh-rHK/strings.xml @@ -71,4 +71,5 @@ 帳號 密碼錯誤 + 安全 diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml index 45ccd3fd..4659d43a 100644 --- a/res/values-zh-rTW/strings.xml +++ b/res/values-zh-rTW/strings.xml @@ -298,4 +298,5 @@ 即時上傳 安全性 影片上傳路徑 + %1$s 目錄的下載未完成 diff --git a/res/values/bools.xml b/res/values/bools.xml index 1c8d68c6..c2ca6732 100644 --- a/res/values/bools.xml +++ b/res/values/bools.xml @@ -2,7 +2,7 @@ 32dp + 128dp diff --git a/res/values/strings.xml b/res/values/strings.xml index 1fc4d8ee..8f2898cb 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -193,6 +193,7 @@ Your server is not returning a correct user id, please contact an administrator Cannot authenticate against this server + Account does not exist in the device yet Keep file up to date Rename @@ -324,6 +325,9 @@ Security Upload Video Path + Download of %1$s folder could not be completed + + %1$s shared \"%2$s\" with you Refresh connection Server address diff --git a/res/values/styles.xml b/res/values/styles.xml index c65cbadd..5996281f 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -3,7 +3,7 @@ ownCloud Android client application Copyright (C) 2012 Bartek Przybylski - Copyright (C) 2012-2013 ownCloud Inc. + 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, diff --git a/res/xml/authenticator.xml b/res/xml/authenticator.xml index eb250052..d0e82440 100644 --- a/res/xml/authenticator.xml +++ b/res/xml/authenticator.xml @@ -3,7 +3,7 @@ ownCloud Android client application Copyright (C) 2012 Bartek Przybylski - Copyright (C) 2012-2013 ownCloud Inc. + 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, diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 1673e21b..ac369847 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -3,7 +3,7 @@ ownCloud Android client application Copyright (C) 2012 Bartek Przybylski - Copyright (C) 2012-2013 ownCloud Inc. + 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, @@ -31,25 +31,23 @@ android:summary="@string/prefs_pincode_summary"/> - - - + - + + - - >>>> x509Certificate " + x509Certificate.toString()); - try { isKnownServer = NetworkUtils.isCertInKnownServersStore((Certificate) x509Certificate, mContext); } catch (Exception e) { @@ -201,36 +194,4 @@ public class SsoWebViewClient extends WebViewClient { ((AuthenticatorActivity)mContext).createAuthenticationDialog(view, handler); } - @Override - public WebResourceResponse shouldInterceptRequest (WebView view, String url) { - Log_OC.d(TAG, "shouldInterceptRequest : " + url); - return null; - } - - @Override - public void onLoadResource (WebView view, String url) { - Log_OC.d(TAG, "onLoadResource : " + url); - } - - @Override - public void onReceivedLoginRequest (WebView view, String realm, String account, String args) { - Log_OC.d(TAG, "onReceivedLoginRequest : " + realm + ", " + account + ", " + args); - } - - @Override - public void onScaleChanged (WebView view, float oldScale, float newScale) { - Log_OC.d(TAG, "onScaleChanged : " + oldScale + " -> " + newScale); - super.onScaleChanged(view, oldScale, newScale); - } - - @Override - public void onUnhandledKeyEvent (WebView view, KeyEvent event) { - Log_OC.d(TAG, "onUnhandledKeyEvent : " + event); - } - - @Override - public boolean shouldOverrideKeyEvent (WebView view, KeyEvent event) { - Log_OC.d(TAG, "shouldOverrideKeyEvent : " + event); - return false; - } } diff --git a/src/com/owncloud/android/datamodel/FileDataStorageManager.java b/src/com/owncloud/android/datamodel/FileDataStorageManager.java index d8030642..a197e080 100644 --- a/src/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/src/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2014 ownCloud Inc. + * 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, @@ -46,6 +48,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 +196,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 +265,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 updatedFiles, Collection filesToRemove @@ -302,6 +306,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 +496,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 +544,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 files = getFolderContent(folder.getFileId()); @@ -549,13 +555,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 +585,6 @@ public class FileDataStorageManager { } else { String path = localFile.getAbsolutePath(); success &= localFile.delete(); - triggerMediaScan(path); // notify MediaScanner about removed file } } } @@ -714,7 +719,7 @@ public class FileDataStorageManager { Iterator 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 +882,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 +1266,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 +1501,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()); + } + } + + } + } diff --git a/src/com/owncloud/android/datamodel/OCFile.java b/src/com/owncloud/android/datamodel/OCFile.java index cf25d278..2c9c53be 100644 --- a/src/com/owncloud/android/datamodel/OCFile.java +++ b/src/com/owncloud/android/datamodel/OCFile.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -20,9 +22,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 +72,8 @@ public class OCFile implements Parcelable, Comparable { private boolean mNeedsUpdateThumbnail; + private boolean mIsDownloading; + /** * Create new {@link OCFile} with given path. @@ -112,6 +116,7 @@ public class OCFile implements Parcelable, Comparable { mPermissions = source.readString(); mRemoteId = source.readString(); mNeedsUpdateThumbnail = source.readInt() == 0; + mIsDownloading = source.readInt() == 0; } @@ -136,6 +141,7 @@ public class OCFile implements Parcelable, Comparable { dest.writeString(mPermissions); dest.writeString(mRemoteId); dest.writeInt(mNeedsUpdateThumbnail ? 1 : 0); + dest.writeInt(mIsDownloading ? 1 : 0); } /** @@ -348,6 +354,7 @@ public class OCFile implements Parcelable, Comparable { mPermissions = null; mRemoteId = null; mNeedsUpdateThumbnail = false; + mIsDownloading = false; } /** @@ -533,17 +540,7 @@ public class OCFile implements Parcelable, Comparable { */ 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 +559,16 @@ public class OCFile implements Parcelable, Comparable { this.mRemoteId = remoteId; } + public boolean isDownloading() { + return mIsDownloading; + } + + public void setDownloading(boolean isDownloading) { + this.mIsDownloading = isDownloading; + } + + public boolean isSynchronizing() { + // TODO real implementation + return false; + } } diff --git a/src/com/owncloud/android/datamodel/ThumbnailsCacheManager.java b/src/com/owncloud/android/datamodel/ThumbnailsCacheManager.java index ce53c444..87fa43ff 100644 --- a/src/com/owncloud/android/datamodel/ThumbnailsCacheManager.java +++ b/src/com/owncloud/android/datamodel/ThumbnailsCacheManager.java @@ -1,5 +1,9 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author Tobias Kaminsky + * @author David A. Velasco + * 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, @@ -29,10 +33,8 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; -import android.graphics.Matrix; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.media.ExifInterface; import android.media.ThumbnailUtils; import android.net.Uri; import android.os.AsyncTask; @@ -51,10 +53,7 @@ import com.owncloud.android.utils.BitmapUtils; import com.owncloud.android.utils.DisplayUtils; /** - * Manager for concurrent access to thumbnails cache. - * - * @author Tobias Kaminsky - * @author David A. Velasco + * Manager for concurrent access to thumbnails cache. */ public class ThumbnailsCacheManager { @@ -76,7 +75,7 @@ public class ThumbnailsCacheManager { public static Bitmap mDefaultImg = BitmapFactory.decodeResource( MainApp.getAppContext().getResources(), - DisplayUtils.getResourceId("image/png", "default.png") + DisplayUtils.getFileTypeIconId("image/png", "default.png") ); @@ -139,44 +138,15 @@ public class ThumbnailsCacheManager { return null; } - - public static boolean cancelPotentialWork(OCFile file, ImageView imageView) { - final ThumbnailGenerationTask bitmapWorkerTask = getBitmapWorkerTask(imageView); - - if (bitmapWorkerTask != null) { - final OCFile bitmapData = bitmapWorkerTask.mFile; - // If bitmapData is not yet set or it differs from the new data - if (bitmapData == null || bitmapData != file) { - // Cancel previous task - bitmapWorkerTask.cancel(true); - } else { - // The same work is already in progress - return false; - } - } - // No task associated with the ImageView, or an existing task was cancelled - return true; - } - - public static ThumbnailGenerationTask getBitmapWorkerTask(ImageView imageView) { - if (imageView != null) { - final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncDrawable) { - final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; - return asyncDrawable.getBitmapWorkerTask(); - } - } - return null; - } - - public static class ThumbnailGenerationTask extends AsyncTask { + public static class ThumbnailGenerationTask extends AsyncTask { private final WeakReference mImageViewReference; private static Account mAccount; - private OCFile mFile; + private Object mFile; private FileDataStorageManager mStorageManager; - + + public ThumbnailGenerationTask(ImageView imageView, FileDataStorageManager storageManager, Account account) { - // Use a WeakReference to ensure the ImageView can be garbage collected + // Use a WeakReference to ensure the ImageView can be garbage collected mImageViewReference = new WeakReference(imageView); if (storageManager == null) throw new IllegalArgumentException("storageManager must not be NULL"); @@ -184,95 +154,46 @@ public class ThumbnailsCacheManager { mAccount = account; } - // Decode image in background. + public ThumbnailGenerationTask(ImageView imageView) { + // Use a WeakReference to ensure the ImageView can be garbage collected + mImageViewReference = new WeakReference(imageView); + } + @Override - protected Bitmap doInBackground(OCFile... params) { + protected Bitmap doInBackground(Object... params) { Bitmap thumbnail = null; - + try { if (mAccount != null) { AccountManager accountMgr = AccountManager.get(MainApp.getAppContext()); - + mServerVersion = accountMgr.getUserData(mAccount, Constants.KEY_OC_VERSION); OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount, MainApp.getAppContext()); mClient = OwnCloudClientManagerFactory.getDefaultSingleton(). getClientFor(ocAccount, MainApp.getAppContext()); } - + mFile = params[0]; - final String imageKey = String.valueOf(mFile.getRemoteId()); - - // Check disk cache in background thread - thumbnail = getBitmapFromDiskCache(imageKey); - - // Not found in disk cache - if (thumbnail == null || mFile.needsUpdateThumbnail()) { - // Converts dp to pixel - Resources r = MainApp.getAppContext().getResources(); - - int px = (int) Math.round(r.getDimension(R.dimen.file_icon_size)); - - if (mFile.isDown()){ - Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile( - mFile.getStoragePath(), px, px); - - if (bitmap != null) { - thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px); - - // Rotate image, obeying exif tag - thumbnail = BitmapUtils.rotateImage(thumbnail, mFile.getStoragePath()); - - // Add thumbnail to cache - addBitmapToCache(imageKey, thumbnail); + + if (mFile instanceof OCFile) { + thumbnail = doOCFileInBackground(); + } else if (mFile instanceof File) { + thumbnail = doFileInBackground(); + } else { + // do nothing + } - mFile.setNeedsUpdateThumbnail(false); - mStorageManager.saveFile(mFile); - } - - } else { - // Download thumbnail from server - if (mClient != null && mServerVersion != null) { - OwnCloudVersion serverOCVersion = new OwnCloudVersion(mServerVersion); - if (serverOCVersion.compareTo(new OwnCloudVersion(MINOR_SERVER_VERSION_FOR_THUMBS)) >= 0) { - try { - int status = -1; - - String uri = mClient.getBaseUri() + "/index.php/apps/files/api/v1/thumbnail/" + - px + "/" + px + Uri.encode(mFile.getRemotePath(), "/"); - Log_OC.d("Thumbnail", "URI: " + uri); - GetMethod get = new GetMethod(uri); - status = mClient.executeMethod(get); - if (status == HttpStatus.SC_OK) { - byte[] bytes = get.getResponseBody(); - Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); - thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px); - - // Add thumbnail to cache - if (thumbnail != null) { - addBitmapToCache(imageKey, thumbnail); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } else { - Log_OC.d(TAG, "Server too old"); - } - } + }catch(Throwable t){ + // the app should never break due to a problem with thumbnails + Log_OC.e(TAG, "Generation of thumbnail for " + mFile + " failed", t); + if (t instanceof OutOfMemoryError) { + System.gc(); } } - - } catch (Throwable t) { - // the app should never break due to a problem with thumbnails - Log_OC.e(TAG, "Generation of thumbnail for " + mFile + " failed", t); - if (t instanceof OutOfMemoryError) { - System.gc(); - } - } - + return thumbnail; } - + protected void onPostExecute(Bitmap bitmap){ if (isCancelled()) { bitmap = null; @@ -280,47 +201,183 @@ public class ThumbnailsCacheManager { if (mImageViewReference != null && bitmap != null) { final ImageView imageView = mImageViewReference.get(); - final ThumbnailGenerationTask bitmapWorkerTask = - getBitmapWorkerTask(imageView); + final ThumbnailGenerationTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (this == bitmapWorkerTask && imageView != null) { - if (imageView.getTag().equals(mFile.getFileId())) { + String tagId = ""; + if (mFile instanceof OCFile){ + tagId = String.valueOf(((OCFile)mFile).getFileId()); + } else if (mFile instanceof File){ + tagId = String.valueOf(((File)mFile).hashCode()); + } + if (String.valueOf(imageView.getTag()).equals(tagId)) { imageView.setImageBitmap(bitmap); } } } } + + /** + * Add thumbnail to cache + * @param imageKey: thumb key + * @param bitmap: image for extracting thumbnail + * @param path: image path + * @param px: thumbnail dp + * @return Bitmap + */ + private Bitmap addThumbnailToCache(String imageKey, Bitmap bitmap, String path, int px){ + + Bitmap thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px); + + // Rotate image, obeying exif tag + thumbnail = BitmapUtils.rotateImage(thumbnail,path); + + // Add thumbnail to cache + addBitmapToCache(imageKey, thumbnail); + + return thumbnail; + } + + /** + * Converts size of file icon from dp to pixel + * @return int + */ + private int getThumbnailDimension(){ + // Converts dp to pixel + Resources r = MainApp.getAppContext().getResources(); + return (int) Math.round(r.getDimension(R.dimen.file_icon_size_grid)); + } + + private Bitmap doOCFileInBackground() { + Bitmap thumbnail = null; + OCFile file = (OCFile)mFile; + + final String imageKey = String.valueOf(file.getRemoteId()); + + // Check disk cache in background thread + thumbnail = getBitmapFromDiskCache(imageKey); + + // Not found in disk cache + if (thumbnail == null || file.needsUpdateThumbnail()) { + + int px = getThumbnailDimension(); + + if (file.isDown()) { + Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile( + file.getStoragePath(), px, px); + + if (bitmap != null) { + thumbnail = addThumbnailToCache(imageKey, bitmap, file.getStoragePath(), px); + + file.setNeedsUpdateThumbnail(false); + mStorageManager.saveFile(file); + } + + } else { + // Download thumbnail from server + if (mClient != null && mServerVersion != null) { + OwnCloudVersion serverOCVersion = new OwnCloudVersion(mServerVersion); + if (serverOCVersion.compareTo(new OwnCloudVersion(MINOR_SERVER_VERSION_FOR_THUMBS)) >= 0) { + try { + int status = -1; + + String uri = mClient.getBaseUri() + "/index.php/apps/files/api/v1/thumbnail/" + + px + "/" + px + Uri.encode(file.getRemotePath(), "/"); + Log_OC.d("Thumbnail", "URI: " + uri); + GetMethod get = new GetMethod(uri); + status = mClient.executeMethod(get); + if (status == HttpStatus.SC_OK) { + byte[] bytes = get.getResponseBody(); + Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px); + + // Add thumbnail to cache + if (thumbnail != null) { + addBitmapToCache(imageKey, thumbnail); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } else { + Log_OC.d(TAG, "Server too old"); + } + } + } + } + + return thumbnail; + + } + + private Bitmap doFileInBackground() { + Bitmap thumbnail = null; + File file = (File)mFile; + + final String imageKey = String.valueOf(file.hashCode()); + + // Check disk cache in background thread + thumbnail = getBitmapFromDiskCache(imageKey); + + // Not found in disk cache + if (thumbnail == null) { + + int px = getThumbnailDimension(); + + Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile( + file.getAbsolutePath(), px, px); + + if (bitmap != null) { + thumbnail = addThumbnailToCache(imageKey, bitmap, file.getPath(), px); + } + } + return thumbnail; + } + } - - + + public static boolean cancelPotentialWork(Object file, ImageView imageView) { + final ThumbnailGenerationTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Object bitmapData = bitmapWorkerTask.mFile; + // If bitmapData is not yet set or it differs from the new data + if (bitmapData == null || bitmapData != file) { + // Cancel previous task + bitmapWorkerTask.cancel(true); + } else { + // The same work is already in progress + return false; + } + } + // No task associated with the ImageView, or an existing task was cancelled + return true; + } + + public static ThumbnailGenerationTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + public static class AsyncDrawable extends BitmapDrawable { private final WeakReference bitmapWorkerTaskReference; public AsyncDrawable( Resources res, Bitmap bitmap, ThumbnailGenerationTask bitmapWorkerTask - ) { - + ) { + super(res, bitmap); bitmapWorkerTaskReference = - new WeakReference(bitmapWorkerTask); + new WeakReference(bitmapWorkerTask); } public ThumbnailGenerationTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); } } - - - /** - * Remove from cache the remoteId passed - * @param fileRemoteId: remote id of mFile passed - */ - public static void removeFileFromCache(String fileRemoteId){ - synchronized (mThumbnailsDiskCacheLock) { - if (mThumbnailCache != null) { - mThumbnailCache.removeKey(fileRemoteId); - } - mThumbnailsDiskCacheLock.notifyAll(); // Wake any waiting threads - } - } - } diff --git a/src/com/owncloud/android/db/DbHandler.java b/src/com/owncloud/android/db/DbHandler.java index 717066b7..66113066 100644 --- a/src/com/owncloud/android/db/DbHandler.java +++ b/src/com/owncloud/android/db/DbHandler.java @@ -1,6 +1,9 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski * Copyright (C) 2011-2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -28,9 +31,6 @@ import android.database.sqlite.SQLiteOpenHelper; /** * Custom database helper for ownCloud - * - * @author Bartek Przybylski - * */ public class DbHandler { private SQLiteDatabase mDB; @@ -114,7 +114,14 @@ public class DbHandler { db.execSQL("ALTER TABLE " + TABLE_INSTANT_UPLOAD + " ADD COLUMN attempt INTEGER;"); } db.execSQL("ALTER TABLE " + TABLE_INSTANT_UPLOAD + " ADD COLUMN message TEXT;"); - + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + //downgrading is the exception, so deleting and re-creating is acceptable. + //otherwise exception will be thrown (cannot downgrade) and oc app will crash. + db.execSQL("DROP TABLE IF EXISTS " + TABLE_INSTANT_UPLOAD + ";"); + onCreate(db); } } } diff --git a/src/com/owncloud/android/db/ProviderMeta.java b/src/com/owncloud/android/db/ProviderMeta.java index bc59869a..1f789d14 100644 --- a/src/com/owncloud/android/db/ProviderMeta.java +++ b/src/com/owncloud/android/db/ProviderMeta.java @@ -1,6 +1,9 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -24,14 +27,11 @@ import com.owncloud.android.MainApp; /** * Meta-Class that holds various static field information - * - * @author Bartek Przybylski - * */ 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"; diff --git a/src/com/owncloud/android/files/BootupBroadcastReceiver.java b/src/com/owncloud/android/files/BootupBroadcastReceiver.java index 4e139aeb..2cf4358d 100644 --- a/src/com/owncloud/android/files/BootupBroadcastReceiver.java +++ b/src/com/owncloud/android/files/BootupBroadcastReceiver.java @@ -1,6 +1,9 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author David A. Velasco * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -29,8 +32,6 @@ import android.content.Intent; /** * App-registered receiver catching the broadcast intent reporting that the system was * just boot up. - * - * @author David A. Velasco */ public class BootupBroadcastReceiver extends BroadcastReceiver { diff --git a/src/com/owncloud/android/files/FileMenuFilter.java b/src/com/owncloud/android/files/FileMenuFilter.java index 6eb746cb..04e9fbc1 100644 --- a/src/com/owncloud/android/files/FileMenuFilter.java +++ b/src/com/owncloud/android/files/FileMenuFilter.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -31,13 +34,13 @@ 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; /** * Filters out the file actions available in a given {@link Menu} for a given {@link OCFile} * according to the current state of the latest. - * - * @author David A. Velasco */ public class FileMenuFilter { @@ -51,8 +54,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 +143,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 +194,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); diff --git a/src/com/owncloud/android/files/FileOperationsHelper.java b/src/com/owncloud/android/files/FileOperationsHelper.java index e1ab1953..46588114 100644 --- a/src/com/owncloud/android/files/FileOperationsHelper.java +++ b/src/com/owncloud/android/files/FileOperationsHelper.java @@ -1,5 +1,9 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author masensio + * @author David A. Velasco + * 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, @@ -41,9 +45,7 @@ import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.dialog.ShareLinkToDialog; /** - * - * @author masensio - * @author David A. Velasco + * */ public class FileOperationsHelper { @@ -127,7 +129,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 +167,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 +199,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 +225,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 +238,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 +251,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 +300,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(); } diff --git a/src/com/owncloud/android/files/InstantUploadBroadcastReceiver.java b/src/com/owncloud/android/files/InstantUploadBroadcastReceiver.java index c1c3e9ca..b52c36d9 100644 --- a/src/com/owncloud/android/files/InstantUploadBroadcastReceiver.java +++ b/src/com/owncloud/android/files/InstantUploadBroadcastReceiver.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2014 ownCloud Inc. + * 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, diff --git a/src/com/owncloud/android/files/services/FileDownloader.java b/src/com/owncloud/android/files/services/FileDownloader.java index c9ad9611..f5be6a76 100644 --- a/src/com/owncloud/android/files/services/FileDownloader.java +++ b/src/com/owncloud/android/files/services/FileDownloader.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * 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,10 +27,9 @@ 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.AccountUtils; import com.owncloud.android.authentication.AuthenticatorActivity; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; @@ -51,7 +52,9 @@ import com.owncloud.android.ui.preview.PreviewImageFragment; import com.owncloud.android.utils.ErrorMessageAdapter; import android.accounts.Account; +import android.accounts.AccountManager; import android.accounts.AccountsException; +import android.accounts.OnAccountsUpdateListener; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; @@ -64,118 +67,159 @@ 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, OnAccountsUpdateListener { -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_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"; private Looper mServiceLooper; private ServiceHandler mServiceHandler; private IBinder mBinder; private OwnCloudClient mDownloadClient = null; - private Account mLastAccount = null; + private Account mCurrentAccount = null; private FileDataStorageManager mStorageManager; - - private ConcurrentMap mPendingDownloads = new ConcurrentHashMap(); + + private IndexedForest mPendingDownloads = new IndexedForest(); + 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 */ @Override public void onCreate() { super.onCreate(); + Log_OC.d(TAG, "Creating service"); mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - HandlerThread thread = new HandlerThread("FileDownloaderThread", - Process.THREAD_PRIORITY_BACKGROUND); + HandlerThread thread = new HandlerThread("FileDownloaderThread", Process.THREAD_PRIORITY_BACKGROUND); thread.start(); mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper, this); mBinder = new FileDownloaderBinder(); + + // add AccountsUpdatedListener + AccountManager am = AccountManager.get(getApplicationContext()); + am.addOnAccountsUpdatedListener(this, null, false); } + + /** + * Service clean up + */ + @Override + public void onDestroy() { + Log_OC.v(TAG, "Destroying service"); + mBinder = null; + mServiceHandler = null; + mServiceLooper.quit(); + mServiceLooper = null; + mNotificationManager = null; + + // remove AccountsUpdatedListener + AccountManager am = AccountManager.get(getApplicationContext()); + am.removeOnAccountsUpdatedListener(this); + + super.onDestroy(); + } + + /** * 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) || + Log_OC.d(TAG, "Starting command with id " + 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 requestedDownloads = new Vector(); // 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 requestedDownloads = new Vector(); + try { + DownloadFileOperation newDownload = new DownloadFileOperation(account, file); + newDownload.addDatatransferProgressListener(this); + newDownload.addDatatransferProgressListener((FileDownloaderBinder) mBinder); + Pair 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 @@ -189,123 +233,173 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis */ @Override public boolean onUnbind(Intent intent) { - ((FileDownloaderBinder)mBinder).clearListeners(); + ((FileDownloaderBinder) mBinder).clearListeners(); return false; // not accepting rebinding (default behaviour) } - + @Override + public void onAccountsUpdated(Account[] accounts) { + //review the current download and cancel it if its account doesn't exist + if (mCurrentDownload != null && + !AccountUtils.exists(mCurrentDownload.getAccount(), getApplicationContext())) { + mCurrentDownload.cancel(); + } + // The rest of downloads are cancelled when they try to start + } + + /** - * Binder to let client components to perform operations on the queue of downloads. - * - * It provides by itself the available operations. + * 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 mBoundListeners = new HashMap(); - - + private Map mBoundListeners = + new HashMap(); + + /** * Cancels a pending or current download of a remote file. - * - * @param account Owncloud account where the remote file is stored. - * @param file A file in the queue of pending downloads + * + * @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 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(); + } } } - - + + /** + * Cancels a pending or current upload for an account + * + * @param account Owncloud accountName where the remote file will be stored. + */ + public void cancel(Account account) { + Log_OC.d(TAG, "Account= " + account.name); + + if (mCurrentDownload != null) { + Log_OC.d(TAG, "Current Download Account= " + mCurrentDownload.getAccount().name); + if (mCurrentDownload.getAccount().name.equals(account.name)) { + mCurrentDownload.cancel(); + } + } + // Cancel pending downloads + cancelDownloadsForAccount(account); + } + public void clearListeners() { mBoundListeners.clear(); } /** - * 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 file A file that could be in the queue of downloads. + * 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 any of its descendant files is downloading or + * waiting to download. + * + * @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 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())); } - + /** * Adds a listener interested in the progress of the download for a concrete file. - * - * @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 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. */ - 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); } - - + + /** * Removes a listener interested in the progress of the download for a concrete file. - * - * @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 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. */ - 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 fileName) { + //String key = buildKey(mCurrentDownload.getAccount(), mCurrentDownload.getFile().getRemotePath()); + OnDatatransferProgressListener boundListener = mBoundListeners.get(mCurrentDownload.getFile().getFileId()); if (boundListener != null) { boundListener.onTransferProgress(progressRate, totalTransferredSoFar, totalToTransfer, fileName); } } - + + /** + * Review downloads and cancel it if its account doesn't exist + */ + public void checkAccountOfCurrentDownload() { + if (mCurrentDownload != null && + !AccountUtils.exists(mCurrentDownload.getAccount(), getApplicationContext())) { + mCurrentDownload.cancel(); + } + // The rest of downloads are cancelled when they try to start + } + } - - - /** - * Download worker. Performs the pending downloads in the order they were requested. - * - * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}. + + + /** + * Download worker. Performs the pending downloads in the order they were requested. + *

+ * Created with the Looper of a new thread, started in {@link FileUploader#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 FileDownloader mService; + public ServiceHandler(Looper looper, FileDownloader service) { super(looper); if (service == null) @@ -320,65 +414,83 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis if (msg.obj != null) { Iterator it = requestedDownloads.iterator(); while (it.hasNext()) { - mService.downloadFile(it.next()); + String next = it.next(); + mService.downloadFile(next); } } + Log_OC.d(TAG, "Stopping after command with id " + msg.arg1); mService.stopSelf(msg.arg1); } } - + /** * Core download method: requests a file to download and stores it. - * - * @param downloadKey Key to access the download to perform, contained in mPendingDownloads + * + * @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); + // Detect if the account exists + if (AccountUtils.exists(mCurrentDownload.getAccount(), getApplicationContext())) { + Log_OC.d(TAG, "Account " + mCurrentDownload.getAccount().name + " exists"); + notifyDownloadStart(mCurrentDownload); - 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); + RemoteOperationResult downloadResult = null; + try { + /// prepare client object to send the request to the ownCloud server + 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 - 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); - downloadResult = new RemoteOperationResult(e); - } catch (IOException e) { - Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e); - downloadResult = new RemoteOperationResult(e); - - } finally { - synchronized(mPendingDownloads) { - mPendingDownloads.remove(downloadKey); + + /// 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 authorization for " + mCurrentAccount.name, e); + downloadResult = new RemoteOperationResult(e); + } catch (IOException e) { + Log_OC.e(TAG, "Error while trying to get authorization for " + mCurrentAccount.name, e); + downloadResult = new RemoteOperationResult(e); + + } finally { + /*Log_OC.v( "NOW " + TAG + ", thread " + Thread.currentThread().getName(), + "Removing payload " + mCurrentDownload.getRemotePath());*/ + + Pair removeResult = + mPendingDownloads.removePayload(mCurrentAccount, mCurrentDownload.getRemotePath()); + + /// notify result + notifyDownloadResult(mCurrentDownload, downloadResult); + + sendBroadcastDownloadFinished(mCurrentDownload, downloadResult, removeResult.second); } - } + } else { + // Cancel the transfer + Log_OC.d(TAG, "Account " + mCurrentDownload.getAccount().toString() + " doesn't exist"); + cancelDownloadsForAccount(mCurrentDownload.getAccount()); - - /// notify result - notifyDownloadResult(mCurrentDownload, downloadResult); - - sendBroadcastDownloadFinished(mCurrentDownload, downloadResult); + } } } @@ -403,16 +515,25 @@ 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 - * - * @param download Download operation starting. + * + * @param download Download operation starting. */ private void notifyDownloadStart(DownloadFileOperation download) { /// create status notification with a progress bar mLastPercent = 0; - mNotificationBuilder = + mNotificationBuilder = NotificationBuilderWithProgressBar.newNotificationBuilderWithProgressBar(this); mNotificationBuilder .setSmallIcon(R.drawable.notification_icon) @@ -424,7 +545,7 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis String.format(getString(R.string.downloader_download_in_progress_content), 0, new File(download.getSavePath()).getName()) ); - + /// includes a pending intent in the notification showing the details view of the file Intent showDetailsIntent = null; if (PreviewImageFragment.canBePreviewed(download.getFile())) { @@ -435,21 +556,21 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, download.getFile()); showDetailsIntent.putExtra(FileActivity.EXTRA_ACCOUNT, download.getAccount()); showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - + mNotificationBuilder.setContentIntent(PendingIntent.getActivity( - this, (int) System.currentTimeMillis(), showDetailsIntent, 0 + this, (int) System.currentTimeMillis(), showDetailsIntent, 0 )); mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotificationBuilder.build()); } - + /** * Callback method to update the progress bar in the status notification. */ @Override public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String filePath) { - int percent = (int)(100.0*((double)totalTransferredSoFar)/((double)totalToTransfer)); + int percent = (int) (100.0 * ((double) totalTransferredSoFar) / ((double) totalToTransfer)); if (percent != mLastPercent) { mNotificationBuilder.setProgress(100, percent, totalToTransfer < 0); String fileName = filePath.substring(filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1); @@ -459,100 +580,120 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis } mLastPercent = percent; } - - + + /** * Updates the status notification with the result of a download operation. - * - * @param downloadResult Result of the download operation. - * @param download Finished download operation + * + * @param downloadResult Result of the download operation. + * @param download Finished download operation */ private void notifyDownloadResult(DownloadFileOperation download, RemoteOperationResult downloadResult) { mNotificationManager.cancel(R.string.downloader_download_in_progress_ticker); if (!downloadResult.isCancelled()) { - int tickerId = (downloadResult.isSuccess()) ? R.string.downloader_download_succeeded_ticker : - R.string.downloader_download_failed_ticker; - + int tickerId = (downloadResult.isSuccess()) ? R.string.downloader_download_succeeded_ticker : + R.string.downloader_download_failed_ticker; + boolean needsToUpdateCredentials = ( downloadResult.getCode() == ResultCode.UNAUTHORIZED || - downloadResult.isIdPRedirection() + downloadResult.isIdPRedirection() ); - tickerId = (needsToUpdateCredentials) ? + tickerId = (needsToUpdateCredentials) ? R.string.downloader_download_failed_credentials_error : tickerId; - + mNotificationBuilder - .setTicker(getString(tickerId)) - .setContentTitle(getString(tickerId)) - .setAutoCancel(true) - .setOngoing(false) - .setProgress(0, 0, false); - + .setTicker(getString(tickerId)) + .setContentTitle(getString(tickerId)) + .setAutoCancel(true) + .setOngoing(false) + .setProgress(0, 0, false); + if (needsToUpdateCredentials) { - + // 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); mNotificationBuilder - .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 - + .setContentIntent(PendingIntent.getActivity( + this, (int) System.currentTimeMillis(), updateAccountCredentials, PendingIntent.FLAG_ONE_SHOT)); + } else { // TODO put something smart in showDetailsIntent - Intent showDetailsIntent = new Intent(); + Intent showDetailsIntent = new Intent(); mNotificationBuilder - .setContentIntent(PendingIntent.getActivity( - this, (int) System.currentTimeMillis(), showDetailsIntent, 0)); + .setContentIntent(PendingIntent.getActivity( + 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 - if (downloadResult.isSuccess()) { + if (downloadResult.isSuccess()) { // Sleep 2 seconds, so show the notification before remove it NotificationDelayer.cancelWithDelay( - mNotificationManager, - R.string.downloader_download_succeeded_ticker, + mNotificationManager, + R.string.downloader_download_succeeded_ticker, 2000); } - + } } - - + + /** * 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); } - - + + /** * 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); } + /** + * Remove downloads of an account + * + * @param account Downloads account to remove + */ + private void cancelDownloadsForAccount(Account account) { + // Cancel pending downloads + mPendingDownloads.remove(account); + } } diff --git a/src/com/owncloud/android/files/services/FileUploader.java b/src/com/owncloud/android/files/services/FileUploader.java index 04804402..851e343f 100644 --- a/src/com/owncloud/android/files/services/FileUploader.java +++ b/src/com/owncloud/android/files/services/FileUploader.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * 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, @@ -31,6 +33,7 @@ import java.util.concurrent.ConcurrentMap; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountsException; +import android.accounts.OnAccountsUpdateListener; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; @@ -76,8 +79,8 @@ import com.owncloud.android.utils.ErrorMessageAdapter; import com.owncloud.android.utils.UriUtils; - -public class FileUploader extends Service implements OnDatatransferProgressListener { +public class FileUploader extends Service + implements OnDatatransferProgressListener, OnAccountsUpdateListener { private static final String UPLOAD_FINISH_MESSAGE = "UPLOAD_FINISH"; public static final String EXTRA_UPLOAD_RESULT = "RESULT"; @@ -124,14 +127,14 @@ public class FileUploader extends Service implements OnDatatransferProgressListe private static final String MIME_TYPE_PDF = "application/pdf"; private static final String FILE_EXTENSION_PDF = ".pdf"; - + public static String getUploadFinishMessage() { return FileUploader.class.getName().toString() + UPLOAD_FINISH_MESSAGE; } - + /** * Builds a key for mPendingUploads from the account and file to upload - * + * * @param account Account where the file to upload is stored * @param file File to upload */ @@ -145,7 +148,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe /** * Checks if an ownCloud server version should support chunked uploads. - * + * * @param version OwnCloud version instance corresponding to an ownCloud * server. * @return 'True' if the ownCloud server with version supports chunked @@ -161,24 +164,50 @@ public class FileUploader extends Service implements OnDatatransferProgressListe @Override public void onCreate() { super.onCreate(); - Log_OC.i(TAG, "mPendingUploads size:" + mPendingUploads.size()); + Log_OC.d(TAG, "Creating service"); mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); HandlerThread thread = new HandlerThread("FileUploaderThread", Process.THREAD_PRIORITY_BACKGROUND); thread.start(); mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper, this); mBinder = new FileUploaderBinder(); + + // add AccountsUpdatedListener + AccountManager am = AccountManager.get(getApplicationContext()); + am.addOnAccountsUpdatedListener(this, null, false); } /** + * Service clean up + */ + @Override + public void onDestroy() { + Log_OC.v(TAG, "Destroying service" ); + mBinder = null; + mServiceHandler = null; + mServiceLooper.quit(); + mServiceLooper = null; + mNotificationManager = null; + + // remove AccountsUpdatedListener + AccountManager am = AccountManager.get(getApplicationContext()); + am.removeOnAccountsUpdatedListener(this); + + super.onDestroy(); + } + + + /** * Entry point to add one or several files to the queue of uploads. - * + * * New uploads 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) { + Log_OC.d(TAG, "Starting command with id " + startId); + if (!intent.hasExtra(KEY_ACCOUNT) || !intent.hasExtra(KEY_UPLOAD_TYPE) || !(intent.hasExtra(KEY_LOCAL_FILE) || intent.hasExtra(KEY_FILE))) { Log_OC.e(TAG, "Not enough information provided in intent"); @@ -199,7 +228,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) }; @@ -229,7 +258,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe boolean forceOverwrite = intent.getBooleanExtra(KEY_FORCE_OVERWRITE, false); boolean isInstant = intent.getBooleanExtra(KEY_INSTANT_UPLOAD, false); int localAction = intent.getIntExtra(KEY_LOCAL_BEHAVIOUR, LOCAL_BEHAVIOUR_COPY); - + if (intent.hasExtra(KEY_FILE) && files == null) { Log_OC.e(TAG, "Incorrect array for OCFiles provided in upload intent"); return Service.START_NOT_STICKY; @@ -262,7 +291,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe AccountManager aMgr = AccountManager.get(this); String version = aMgr.getUserData(account, Constants.KEY_OC_VERSION); OwnCloudVersion ocv = new OwnCloudVersion(version); - + boolean chunked = FileUploader.chunkedUploadIsSupported(ocv); AbstractList requestedUploads = new Vector(); String uploadKey = null; @@ -270,7 +299,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe try { for (int i = 0; i < files.length; i++) { uploadKey = buildRemoteName(account, files[i].getRemotePath()); - newUpload = new UploadFileOperation(account, files[i], chunked, isInstant, forceOverwrite, localAction, + newUpload = new UploadFileOperation(account, files[i], chunked, isInstant, forceOverwrite, localAction, getApplicationContext()); if (isInstant) { newUpload.setRemoteFolderToBeCreated(); @@ -309,7 +338,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe /** * Provides a binder object that clients can use to perform operations on * the queue of uploads, excepting the addition of new files. - * + * * Implemented to perform cancellation, pause and resume of existing * uploads. */ @@ -317,7 +346,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe public IBinder onBind(Intent arg0) { return mBinder; } - + /** * Called when ALL the bound clients were onbound. */ @@ -326,24 +355,33 @@ public class FileUploader extends Service implements OnDatatransferProgressListe ((FileUploaderBinder)mBinder).clearListeners(); return false; // not accepting rebinding (default behaviour) } - + + @Override + public void onAccountsUpdated(Account[] accounts) { + // Review current upload, and cancel it if its account doen't exist + if (mCurrentUpload != null && + !AccountUtils.exists(mCurrentUpload.getAccount(), getApplicationContext())) { + mCurrentUpload.cancel(); + } + // The rest of uploads are cancelled when they try to start + } /** * Binder to let client components to perform operations on the queue of * uploads. - * + * * It provides by itself the available operations. */ public class FileUploaderBinder extends Binder implements OnDatatransferProgressListener { - - /** + + /** * Map of listeners that will be reported about progress of uploads from a {@link FileUploaderBinder} instance */ private Map mBoundListeners = new HashMap(); - + /** * Cancels a pending or current upload of a remote file. - * + * * @param account Owncloud account where the remote file will be stored. * @param file A file in the queue of pending uploads */ @@ -356,24 +394,37 @@ public class FileUploader extends Service implements OnDatatransferProgressListe upload.cancel(); } } - - - + + /** + * Cancels a pending or current upload for an account + * + * @param account Owncloud accountName where the remote file will be stored. + */ + public void cancel(Account account) { + Log_OC.d(TAG, "Account= " + account.name); + + if (mCurrentUpload != null) { + Log_OC.d(TAG, "Current Upload Account= " + mCurrentUpload.getAccount().name); + if (mCurrentUpload.getAccount().name.equals(account.name)) { + mCurrentUpload.cancel(); + } + } + // Cancel pending uploads + cancelUploadForAccount(account.name); + } + public void clearListeners() { mBoundListeners.clear(); } - - - /** * Returns True when the file described by 'file' is being uploaded to * the ownCloud account 'account' or waiting for it - * + * * 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) @@ -397,25 +448,25 @@ public class FileUploader extends Service implements OnDatatransferProgressListe /** * Adds a listener interested in the progress of the upload for a concrete file. - * + * * @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; String targetKey = buildRemoteName(account, file); mBoundListeners.put(targetKey, listener); } - - - + + + /** * Removes a listener interested in the progress of the upload for a concrete file. - * + * * @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; @@ -428,20 +479,30 @@ public class FileUploader extends Service implements OnDatatransferProgressListe @Override public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, - String fileName) { + String fileName) { String key = buildRemoteName(mCurrentUpload.getAccount(), mCurrentUpload.getFile()); OnDatatransferProgressListener boundListener = mBoundListeners.get(key); if (boundListener != null) { boundListener.onTransferProgress(progressRate, totalTransferredSoFar, totalToTransfer, fileName); } } - + + /** + * Review uploads and cancel it if its account doesn't exist + */ + public void checkAccountOfCurrentUpload() { + if (mCurrentUpload != null && + !AccountUtils.exists(mCurrentUpload.getAccount(), getApplicationContext())) { + mCurrentUpload.cancel(); + } + // The rest of uploads are cancelled when they try to start + } } /** * Upload worker. Performs the pending uploads in the order they were * requested. - * + * * Created with the Looper of a new thread, started in * {@link FileUploader#onCreate()}. */ @@ -467,13 +528,14 @@ public class FileUploader extends Service implements OnDatatransferProgressListe mService.uploadFile(it.next()); } } + Log_OC.d(TAG, "Stopping command after id " + msg.arg1); mService.stopSelf(msg.arg1); } } /** * Core upload method: sends the file(s) to upload - * + * * @param uploadKey Key to access the upload to perform, contained in * mPendingUploads */ @@ -485,63 +547,73 @@ public class FileUploader extends Service implements OnDatatransferProgressListe if (mCurrentUpload != null) { - notifyUploadStart(mCurrentUpload); + // Detect if the account exists + if (AccountUtils.exists(mCurrentUpload.getAccount(), getApplicationContext())) { + Log_OC.d(TAG, "Account " + mCurrentUpload.getAccount().name + " exists"); - RemoteOperationResult uploadResult = null, grantResult = null; - - try { - /// prepare client object to send requests to the ownCloud server - if (mUploadClient == null || !mLastAccount.equals(mCurrentUpload.getAccount())) { - mLastAccount = mCurrentUpload.getAccount(); - mStorageManager = - new FileDataStorageManager(mLastAccount, getContentResolver()); - OwnCloudAccount ocAccount = new OwnCloudAccount(mLastAccount, this); - mUploadClient = OwnCloudClientManagerFactory.getDefaultSingleton(). - getClientFor(ocAccount, this); - } - - /// check the existence of the parent folder for the file to upload - String remoteParentPath = new File(mCurrentUpload.getRemotePath()).getParent(); - remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ? remoteParentPath : remoteParentPath + OCFile.PATH_SEPARATOR; - grantResult = grantFolderExistence(remoteParentPath); - - /// perform the upload - if (grantResult.isSuccess()) { - OCFile parent = mStorageManager.getFileByPath(remoteParentPath); - mCurrentUpload.getFile().setParentId(parent.getFileId()); - uploadResult = mCurrentUpload.execute(mUploadClient); - if (uploadResult.isSuccess()) { - saveUploadedFile(); + notifyUploadStart(mCurrentUpload); + + RemoteOperationResult uploadResult = null, grantResult = null; + + try { + /// prepare client object to send requests to the ownCloud server + if (mUploadClient == null || !mLastAccount.equals(mCurrentUpload.getAccount())) { + mLastAccount = mCurrentUpload.getAccount(); + mStorageManager = + new FileDataStorageManager(mLastAccount, getContentResolver()); + OwnCloudAccount ocAccount = new OwnCloudAccount(mLastAccount, this); + mUploadClient = OwnCloudClientManagerFactory.getDefaultSingleton(). + getClientFor(ocAccount, this); + } + + /// check the existence of the parent folder for the file to upload + String remoteParentPath = new File(mCurrentUpload.getRemotePath()).getParent(); + remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ? + remoteParentPath : remoteParentPath + OCFile.PATH_SEPARATOR; + grantResult = grantFolderExistence(remoteParentPath); + + /// perform the upload + if (grantResult.isSuccess()) { + OCFile parent = mStorageManager.getFileByPath(remoteParentPath); + mCurrentUpload.getFile().setParentId(parent.getFileId()); + uploadResult = mCurrentUpload.execute(mUploadClient); + if (uploadResult.isSuccess()) { + saveUploadedFile(); + } + } else { + uploadResult = grantResult; + } + + } catch (AccountsException e) { + Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e); + uploadResult = new RemoteOperationResult(e); + + } catch (IOException e) { + Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e); + uploadResult = new RemoteOperationResult(e); + + } finally { + synchronized (mPendingUploads) { + mPendingUploads.remove(uploadKey); + Log_OC.i(TAG, "Remove CurrentUploadItem from pending upload Item Map."); + } + if (uploadResult.isException()) { + // enforce the creation of a new client object for next uploads; this grant that a new socket will + // be created in the future if the current exception is due to an abrupt lose of network connection + mUploadClient = null; } - } else { - uploadResult = grantResult; - } - - } catch (AccountsException e) { - Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e); - uploadResult = new RemoteOperationResult(e); - - } catch (IOException e) { - Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e); - uploadResult = new RemoteOperationResult(e); - - } finally { - synchronized (mPendingUploads) { - mPendingUploads.remove(uploadKey); - Log_OC.i(TAG, "Remove CurrentUploadItem from pending upload Item Map."); - } - if (uploadResult.isException()) { - // enforce the creation of a new client object for next uploads; this grant that a new socket will - // be created in the future if the current exception is due to an abrupt lose of network connection - mUploadClient = null; } - } - - /// notify result - - notifyUploadResult(uploadResult, mCurrentUpload); - sendFinalBroadcast(mCurrentUpload, uploadResult); + /// notify result + notifyUploadResult(uploadResult, mCurrentUpload); + sendFinalBroadcast(mCurrentUpload, uploadResult); + + } else { + // Cancel the transfer + Log_OC.d(TAG, "Account " + mCurrentUpload.getAccount().toString() + " doesn't exist"); + cancelUploadForAccount(mCurrentUpload.getAccount().name); + + } } } @@ -549,17 +621,18 @@ public class FileUploader extends Service implements OnDatatransferProgressListe /** * Checks the existence of the folder where the current file will be uploaded both in the remote server * and in the local database. - * + * * If the upload is set to enforce the creation of the folder, the method tries to create it both remote * and locally. - * + * * @param pathToGrant Full remote path whose existence will be granted. * @return An {@link OCFile} instance corresponding to the folder where the file will be uploaded. */ private RemoteOperationResult grantFolderExistence(String pathToGrant) { RemoteOperation operation = new ExistenceCheckRemoteOperation(pathToGrant, this, false); RemoteOperationResult result = operation.execute(mUploadClient); - if (!result.isSuccess() && result.getCode() == ResultCode.FILE_NOT_FOUND && mCurrentUpload.isRemoteFolderToBeCreated()) { + if (!result.isSuccess() && result.getCode() == ResultCode.FILE_NOT_FOUND && + mCurrentUpload.isRemoteFolderToBeCreated()) { SyncOperation syncOp = new CreateFolderOperation( pathToGrant, true); result = syncOp.execute(mUploadClient, mStorageManager); } @@ -577,10 +650,11 @@ public class FileUploader extends Service implements OnDatatransferProgressListe return result; } - + private OCFile createLocalFolder(String remotePath) { String parentPath = new File(remotePath).getParent(); - parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ? parentPath : parentPath + OCFile.PATH_SEPARATOR; + parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ? + parentPath : parentPath + OCFile.PATH_SEPARATOR; OCFile parent = mStorageManager.getFileByPath(parentPath); if (parent == null) { parent = createLocalFolder(parentPath); @@ -594,15 +668,15 @@ public class FileUploader extends Service implements OnDatatransferProgressListe } return null; } - + /** * Saves a OC File after a successful upload. - * + * * A PROPFIND is necessary to keep the props in the local database * synchronized with the server, specially the modification time and Etag * (where available) - * + * * TODO refactor this ugly thing */ private void saveUploadedFile() { @@ -621,7 +695,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe updateOCFile(file, (RemoteFile) result.getData().get(0)); file.setLastSyncDateForProperties(syncDate); } - + // / maybe this would be better as part of UploadFileOperation... or // maybe all this method if (mCurrentUpload.wasRenamed()) { @@ -631,8 +705,8 @@ public class FileUploader extends Service implements OnDatatransferProgressListe mStorageManager.saveFile(oldFile); } // else: it was just an automatic renaming due to a name - // coincidence; nothing else is needed, the storagePath is right - // in the instance returned by mCurrentUpload.getFile() + // coincidence; nothing else is needed, the storagePath is right + // in the instance returned by mCurrentUpload.getFile() } file.setNeedsUpdateThumbnail(true); mStorageManager.saveFile(file); @@ -649,7 +723,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe } private OCFile obtainNewOCFileToUpload(String remotePath, String localPath, String mimeType, - FileDataStorageManager storageManager) { + FileDataStorageManager storageManager) { // MIME type if (mimeType == null || mimeType.length() <= 0) { @@ -679,7 +753,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe newFile.setFileLength(localFile.length()); newFile.setLastSyncDateForData(localFile.lastModified()); } // don't worry about not assigning size, the problems with localPath - // are checked when the UploadFileOperation instance is created + // are checked when the UploadFileOperation instance is created newFile.setMimetype(mimeType); @@ -689,13 +763,13 @@ public class FileUploader extends Service implements OnDatatransferProgressListe /** * Creates a status notification to show the upload progress - * + * * @param upload Upload operation starting. */ private void notifyUploadStart(UploadFileOperation upload) { // / create status notification with a progress bar mLastPercent = 0; - mNotificationBuilder = + mNotificationBuilder = NotificationBuilderWithProgressBar.newNotificationBuilderWithProgressBar(this); mNotificationBuilder .setOngoing(true) @@ -712,7 +786,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe showDetailsIntent.putExtra(FileActivity.EXTRA_ACCOUNT, upload.getAccount()); showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); mNotificationBuilder.setContentIntent(PendingIntent.getActivity( - this, (int) System.currentTimeMillis(), showDetailsIntent, 0 + this, (int) System.currentTimeMillis(), showDetailsIntent, 0 )); mNotificationManager.notify(R.string.uploader_upload_in_progress_ticker, mNotificationBuilder.build()); @@ -736,7 +810,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe /** * Updates the status notification with the result of an upload operation. - * + * * @param uploadResult Result of the upload operation. * @param upload Finished upload operation */ @@ -745,33 +819,33 @@ public class FileUploader extends Service implements OnDatatransferProgressListe Log_OC.d(TAG, "NotifyUploadResult with resultCode: " + uploadResult.getCode()); // / cancelled operation or success -> silent removal of progress notification mNotificationManager.cancel(R.string.uploader_upload_in_progress_ticker); - + // Show the result: success or fail notification if (!uploadResult.isCancelled()) { - int tickerId = (uploadResult.isSuccess()) ? R.string.uploader_upload_succeeded_ticker : - R.string.uploader_upload_failed_ticker; - + int tickerId = (uploadResult.isSuccess()) ? R.string.uploader_upload_succeeded_ticker : + R.string.uploader_upload_failed_ticker; + String content = null; // check credentials error boolean needsToUpdateCredentials = ( - uploadResult.getCode() == ResultCode.UNAUTHORIZED || - uploadResult.isIdPRedirection() + uploadResult.getCode() == ResultCode.UNAUTHORIZED || + uploadResult.isIdPRedirection() ); - tickerId = (needsToUpdateCredentials) ? + tickerId = (needsToUpdateCredentials) ? R.string.uploader_upload_failed_credentials_error : tickerId; mNotificationBuilder - .setTicker(getString(tickerId)) - .setContentTitle(getString(tickerId)) - .setAutoCancel(true) - .setOngoing(false) - .setProgress(0, 0, false); - + .setTicker(getString(tickerId)) + .setContentTitle(getString(tickerId)) + .setAutoCancel(true) + .setOngoing(false) + .setProgress(0, 0, false); + content = ErrorMessageAdapter.getErrorCauseMessage( uploadResult, upload, getResources() ); - + if (needsToUpdateCredentials) { // let the user update credentials with one click Intent updateAccountCredentials = new Intent(this, AuthenticatorActivity.class); @@ -779,24 +853,24 @@ public class FileUploader extends Service implements OnDatatransferProgressListe AuthenticatorActivity.EXTRA_ACCOUNT, upload.getAccount() ); updateAccountCredentials.putExtra( - AuthenticatorActivity.EXTRA_ACTION, + 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); mNotificationBuilder.setContentIntent(PendingIntent.getActivity( - this, - (int) System.currentTimeMillis(), - updateAccountCredentials, - PendingIntent.FLAG_ONE_SHOT + this, + (int) System.currentTimeMillis(), + updateAccountCredentials, + PendingIntent.FLAG_ONE_SHOT )); - - mUploadClient = null; - // grant that future retries on the same account will get the fresh credentials + + mUploadClient = null; + // grant that future retries on the same account will get the fresh credentials } else { mNotificationBuilder.setContentText(content); - + if (upload.isInstant()) { DbHandler db = null; try { @@ -807,12 +881,12 @@ public class FileUploader extends Service implements OnDatatransferProgressListe if (uploadResult.getCode() == ResultCode.QUOTA_EXCEEDED) { //message = getString(R.string.failed_upload_quota_exceeded_text); if (db.updateFileState( - upload.getOriginalStoragePath(), + upload.getOriginalStoragePath(), DbHandler.UPLOAD_STATUS_UPLOAD_FAILED, message) == 0) { db.putFileForLater( - upload.getOriginalStoragePath(), - upload.getAccount().name, + upload.getOriginalStoragePath(), + upload.getAccount().name, message ); } @@ -824,22 +898,22 @@ public class FileUploader extends Service implements OnDatatransferProgressListe } } } - + mNotificationBuilder.setContentText(content); mNotificationManager.notify(tickerId, mNotificationBuilder.build()); - + if (uploadResult.isSuccess()) { - + DbHandler db = new DbHandler(this.getBaseContext()); db.removeIUPendingFile(mCurrentUpload.getOriginalStoragePath()); db.close(); // remove success notification, with a delay of 2 seconds NotificationDelayer.cancelWithDelay( - mNotificationManager, - R.string.uploader_upload_succeeded_ticker, + mNotificationManager, + R.string.uploader_upload_succeeded_ticker, 2000); - + } } } @@ -847,17 +921,17 @@ public class FileUploader extends Service implements OnDatatransferProgressListe /** * Sends a broadcast in order to the interested activities can update their * view - * + * * @param upload Finished upload operation * @param uploadResult Result of the upload operation */ private void sendFinalBroadcast(UploadFileOperation upload, RemoteOperationResult uploadResult) { Intent end = new Intent(getUploadFinishMessage()); end.putExtra(EXTRA_REMOTE_PATH, upload.getRemotePath()); // real remote - // path, after - // possible - // automatic - // renaming + // path, after + // possible + // automatic + // renaming if (upload.wasRenamed()) { end.putExtra(EXTRA_OLD_REMOTE_PATH, upload.getOldFile().getRemotePath()); } @@ -875,9 +949,27 @@ public class FileUploader extends Service implements OnDatatransferProgressListe * @return true if is needed to add the pdf file extension to the file */ private boolean isPdfFileFromContentProviderWithoutExtension(String localPath, String mimeType) { - return localPath.startsWith(UriUtils.URI_CONTENT_SCHEME) && - mimeType.equals(MIME_TYPE_PDF) && + return localPath.startsWith(UriUtils.URI_CONTENT_SCHEME) && + mimeType.equals(MIME_TYPE_PDF) && !localPath.endsWith(FILE_EXTENSION_PDF); } + /** + * Remove uploads of an account + * @param accountName + */ + private void cancelUploadForAccount(String accountName){ + // this can be slow if there are many uploads :( + Iterator it = mPendingUploads.keySet().iterator(); + Log_OC.d(TAG, "Number of pending updloads= " + mPendingUploads.size()); + while (it.hasNext()) { + String key = it.next(); + Log_OC.d(TAG, "mPendingUploads CANCELLED " + key); + if (key.startsWith(accountName)) { + synchronized (mPendingUploads) { + mPendingUploads.remove(key); + } + } + } + } } diff --git a/src/com/owncloud/android/files/services/IndexedForest.java b/src/com/owncloud/android/files/services/IndexedForest.java new file mode 100644 index 00000000..4c1ac7bd --- /dev/null +++ b/src/com/owncloud/android/files/services/IndexedForest.java @@ -0,0 +1,240 @@ +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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 . + * + */ + +package com.owncloud.android.files.services; + +import android.accounts.Account; +import android.util.Pair; + +import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.lib.common.utils.Log_OC; + +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. + */ +public class IndexedForest { + + private ConcurrentMap> mMap = new ConcurrentHashMap>(); + + private class Node { + String mKey = null; + Node mParent = null; + Set> mChildren = new HashSet>(); // 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 getParent() { + return mParent; + }; + + public Set> getChildren() { + return mChildren; + } + + public String getKey() { + return mKey; + } + + public V getPayload() { + return mPayload; + } + + public void addChild(Node child) { + mChildren.add(child); + child.setParent(this); + } + + private void setParent(Node parent) { + mParent = parent; + } + + public boolean hasChildren() { + return mChildren.size() > 0; + } + + public void removeChild(Node removed) { + mChildren.remove(removed); + } + + public void clearPayload() { + mPayload = null; + } + } + + + public /* synchronized */ Pair putIfAbsent(Account account, String remotePath, V value) { + String targetKey = buildKey(account, remotePath); + Node valuedNode = new Node(targetKey, value); + mMap.putIfAbsent( + targetKey, + valuedNode + ); + + String currentPath = remotePath, parentPath = null, parentKey = null; + Node 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(targetKey, linkedTo); + }; + + + public Pair removePayload(Account account, String remotePath) { + String targetKey = buildKey(account, remotePath); + Node target = mMap.get(targetKey); + if (target != null) { + target.clearPayload(); + if (!target.hasChildren()) { + return remove(account, remotePath); + } + } + return new Pair(null, null); + } + + + public /* synchronized */ Pair remove(Account account, String remotePath) { + String targetKey = buildKey(account, remotePath); + Node firstRemoved = mMap.remove(targetKey); + String unlinkedFrom = null; + + if (firstRemoved != null) { + /// remove children + removeDescendants(firstRemoved); + + /// remove ancestors if only here due to firstRemoved + Node removed = firstRemoved; + Node 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(firstRemoved.getPayload(), unlinkedFrom); + } + + return new Pair(null, null); + } + + private void removeDescendants(Node removed) { + Iterator> childrenIt = removed.getChildren().iterator(); + Node 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 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); + } + + + /** + * Remove the elements that contains account as a part of its key + * @param account + */ + public void remove(Account account){ + Iterator it = mMap.keySet().iterator(); + while (it.hasNext()) { + String key = it.next(); + Log_OC.d("IndexedForest", "Number of pending downloads= " + mMap.size()); + if (key.startsWith(account.name)) { + mMap.remove(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; + } + +} diff --git a/src/com/owncloud/android/files/services/OnUploadCompletedListener.java b/src/com/owncloud/android/files/services/OnUploadCompletedListener.java index b6ee1aca..f2ed3bbe 100644 --- a/src/com/owncloud/android/files/services/OnUploadCompletedListener.java +++ b/src/com/owncloud/android/files/services/OnUploadCompletedListener.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, diff --git a/src/com/owncloud/android/media/MediaControlView.java b/src/com/owncloud/android/media/MediaControlView.java index b257bd37..653943b9 100644 --- a/src/com/owncloud/android/media/MediaControlView.java +++ b/src/com/owncloud/android/media/MediaControlView.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application - * - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -51,8 +53,6 @@ import com.owncloud.android.R; * * It synchronizes itself with the state of the * {@link MediaPlayer}. - * - * @author David A. Velasco */ public class MediaControlView extends FrameLayout /* implements OnLayoutChangeListener, OnTouchListener */ implements OnClickListener, OnSeekBarChangeListener { diff --git a/src/com/owncloud/android/media/MediaService.java b/src/com/owncloud/android/media/MediaService.java index 52daa04c..e53c635f 100644 --- a/src/com/owncloud/android/media/MediaService.java +++ b/src/com/owncloud/android/media/MediaService.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -49,8 +52,6 @@ import com.owncloud.android.ui.activity.FileDisplayActivity; * * Waits for Intents which signal the service to perform specific operations: Play, Pause, * Rewind, etc. - * - * @author David A. Velasco */ public class MediaService extends Service implements OnCompletionListener, OnPreparedListener, OnErrorListener, AudioManager.OnAudioFocusChangeListener { diff --git a/src/com/owncloud/android/media/MediaServiceBinder.java b/src/com/owncloud/android/media/MediaServiceBinder.java index 1b56ec01..95bf5207 100644 --- a/src/com/owncloud/android/media/MediaServiceBinder.java +++ b/src/com/owncloud/android/media/MediaServiceBinder.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -34,8 +37,6 @@ import android.widget.MediaController; * * Provides the operations of {@link MediaController.MediaPlayerControl}, and an extra method to check if * an {@link OCFile} instance is handled by the MediaService. - * - * @author David A. Velasco */ public class MediaServiceBinder extends Binder implements MediaController.MediaPlayerControl { diff --git a/src/com/owncloud/android/notifications/NotificationBuilderWithProgressBar.java b/src/com/owncloud/android/notifications/NotificationBuilderWithProgressBar.java index 63ddf635..c71b35f2 100644 --- a/src/com/owncloud/android/notifications/NotificationBuilderWithProgressBar.java +++ b/src/com/owncloud/android/notifications/NotificationBuilderWithProgressBar.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -31,8 +34,6 @@ import android.widget.RemoteViews; * a progress bar is available in every Android version, because * {@link NotificationCompat.Builder#setProgress(int, int, boolean)} has no * real effect for Android < 4.0 - * - * @author David A. Velasco */ public class NotificationBuilderWithProgressBar extends NotificationCompat.Builder { diff --git a/src/com/owncloud/android/notifications/NotificationDelayer.java b/src/com/owncloud/android/notifications/NotificationDelayer.java index aeefe12c..ab1399f1 100644 --- a/src/com/owncloud/android/notifications/NotificationDelayer.java +++ b/src/com/owncloud/android/notifications/NotificationDelayer.java @@ -1,3 +1,22 @@ +/** + * 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 . + * + */ + package com.owncloud.android.notifications; import java.util.Random; diff --git a/src/com/owncloud/android/operations/CreateFolderOperation.java b/src/com/owncloud/android/operations/CreateFolderOperation.java index 4df8b3df..b3c17f1d 100644 --- a/src/com/owncloud/android/operations/CreateFolderOperation.java +++ b/src/com/owncloud/android/operations/CreateFolderOperation.java @@ -1,5 +1,9 @@ -/* ownCloud Android client application - * Copyright (C) 2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * @author masensio + * 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, @@ -31,9 +35,6 @@ import com.owncloud.android.utils.FileStorageUtils; /** * Access to remote operation performing the creation of a new folder in the ownCloud server. * Save the new folder in Database - * - * @author David A. Velasco - * @author masensio */ public class CreateFolderOperation extends SyncOperation implements OnRemoteOperationListener{ diff --git a/src/com/owncloud/android/operations/CreateShareOperation.java b/src/com/owncloud/android/operations/CreateShareOperation.java index b563790f..01059c9e 100644 --- a/src/com/owncloud/android/operations/CreateShareOperation.java +++ b/src/com/owncloud/android/operations/CreateShareOperation.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author masensio + * 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, @@ -19,13 +22,12 @@ package com.owncloud.android.operations; /** * Creates a new share from a given file - * - * @author masensio - * */ +import android.content.Context; import android.content.Intent; +import com.owncloud.android.R; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.lib.common.OwnCloudClient; @@ -46,6 +48,7 @@ public class CreateShareOperation extends SyncOperation { protected FileDataStorageManager mStorageManager; + private Context mContext; private String mPath; private ShareType mShareType; private String mShareWith; @@ -56,6 +59,7 @@ public class CreateShareOperation extends SyncOperation { /** * Constructor + * @param context The context that the share is coming from. * @param path Full path of the file/folder being shared. Mandatory argument * @param shareType 0 = user, 1 = group, 3 = Public link. Mandatory argument * @param shareWith User/group ID with who the file should be shared. This is mandatory for shareType of 0 or 1 @@ -72,9 +76,10 @@ public class CreateShareOperation extends SyncOperation { * To obtain combinations, add the desired values together. * For instance, for Re-Share, delete, read, update, add 16+8+2+1 = 27. */ - public CreateShareOperation(String path, ShareType shareType, String shareWith, boolean publicUpload, + public CreateShareOperation(Context context, String path, ShareType shareType, String shareWith, boolean publicUpload, String password, int permissions, Intent sendIntent) { + mContext = context; mPath = path; mShareType = shareType; mShareWith = shareWith; @@ -128,6 +133,8 @@ public class CreateShareOperation extends SyncOperation { OCFile file = getStorageManager().getFileByPath(mPath); if (file!=null) { mSendIntent.putExtra(Intent.EXTRA_TEXT, share.getShareLink()); + mSendIntent.putExtra(Intent.EXTRA_SUBJECT, String.format(mContext.getString(R.string.subject_token), + getClient().getCredentials().getUsername(), file.getFileName())); file.setPublicLink(share.getShareLink()); file.setShareByLink(true); getStorageManager().saveFile(file); diff --git a/src/com/owncloud/android/operations/DetectAuthenticationMethodOperation.java b/src/com/owncloud/android/operations/DetectAuthenticationMethodOperation.java index 5afc4210..9f2827f4 100644 --- a/src/com/owncloud/android/operations/DetectAuthenticationMethodOperation.java +++ b/src/com/owncloud/android/operations/DetectAuthenticationMethodOperation.java @@ -1,24 +1,20 @@ -/* ownCloud Android Library is available under MIT license - * Copyright (C) 2014 ownCloud Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS - * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN - * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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 . * */ @@ -47,9 +43,7 @@ import android.net.Uri; * When successful, the instance of {@link RemoteOperationResult} passed * through {@link OnRemoteOperationListener#onRemoteOperationFinish(RemoteOperation, * RemoteOperationResult)} returns in {@link RemoteOperationResult#getData()} - * a value of {@link AuthenticationMethod}. - * - * @author David A. Velasco + * a value of {@link AuthenticationMethod}. */ public class DetectAuthenticationMethodOperation extends RemoteOperation { diff --git a/src/com/owncloud/android/operations/DownloadFileOperation.java b/src/com/owncloud/android/operations/DownloadFileOperation.java index 0a5ff94c..78811f8b 100644 --- a/src/com/owncloud/android/operations/DownloadFileOperation.java +++ b/src/com/owncloud/android/operations/DownloadFileOperation.java @@ -1,5 +1,9 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * @author masensio + * 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, @@ -38,9 +42,6 @@ import android.webkit.MimeTypeMap; /** * Remote mDownloadOperation performing the download of a file to an ownCloud server - * - * @author David A. Velasco - * @author masensio */ public class DownloadFileOperation extends RemoteOperation { @@ -177,5 +178,4 @@ public class DownloadFileOperation extends RemoteOperation { mDataTransferListeners.remove(listener); } } - } diff --git a/src/com/owncloud/android/operations/GetServerInfoOperation.java b/src/com/owncloud/android/operations/GetServerInfoOperation.java index 9b7cf872..e081d4eb 100644 --- a/src/com/owncloud/android/operations/GetServerInfoOperation.java +++ b/src/com/owncloud/android/operations/GetServerInfoOperation.java @@ -1,24 +1,21 @@ -/* ownCloud Android Library is available under MIT license - * Copyright (C) 2014 ownCloud Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS - * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN - * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * @author masensio + * 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 . * */ @@ -43,9 +40,6 @@ import android.content.Context; * * Checks the existence of a configured ownCloud server in the URL, gets its version * and finds out what authentication method is needed to access files in it. - * - * @author David A. Velasco - * @author masensio */ public class GetServerInfoOperation extends RemoteOperation { diff --git a/src/com/owncloud/android/operations/GetSharesForFileOperation.java b/src/com/owncloud/android/operations/GetSharesForFileOperation.java index 649437d2..06e399e8 100644 --- a/src/com/owncloud/android/operations/GetSharesForFileOperation.java +++ b/src/com/owncloud/android/operations/GetSharesForFileOperation.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author masensio + * 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, @@ -28,10 +31,7 @@ import com.owncloud.android.lib.resources.shares.GetRemoteSharesForFileOperation import com.owncloud.android.operations.common.SyncOperation; /** - * Provide a list shares for a specific file. - * - * @author masensio - * + * Provide a list shares for a specific file. */ public class GetSharesForFileOperation extends SyncOperation { diff --git a/src/com/owncloud/android/operations/GetSharesOperation.java b/src/com/owncloud/android/operations/GetSharesOperation.java index d096788a..fb838b4d 100644 --- a/src/com/owncloud/android/operations/GetSharesOperation.java +++ b/src/com/owncloud/android/operations/GetSharesOperation.java @@ -1,5 +1,9 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author masensio + * @author David A. Velasco + * 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, @@ -29,9 +33,6 @@ import com.owncloud.android.operations.common.SyncOperation; /** * Access to remote operation to get the share files/folders * Save the data in Database - * - * @author masensio - * @author David A. Velasco */ public class GetSharesOperation extends SyncOperation { diff --git a/src/com/owncloud/android/operations/MoveFileOperation.java b/src/com/owncloud/android/operations/MoveFileOperation.java index 63856c37..3a1103b1 100644 --- a/src/com/owncloud/android/operations/MoveFileOperation.java +++ b/src/com/owncloud/android/operations/MoveFileOperation.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -29,8 +32,6 @@ import android.accounts.Account; /** * Operation mmoving an {@link OCFile} to a different folder. - * - * @author David A. Velasco */ public class MoveFileOperation extends SyncOperation { diff --git a/src/com/owncloud/android/operations/OAuth2GetAccessToken.java b/src/com/owncloud/android/operations/OAuth2GetAccessToken.java index 6f4ff741..918c57bb 100644 --- a/src/com/owncloud/android/operations/OAuth2GetAccessToken.java +++ b/src/com/owncloud/android/operations/OAuth2GetAccessToken.java @@ -1,3 +1,22 @@ +/** + * 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 . + * + */ + package com.owncloud.android.operations; import java.util.ArrayList; diff --git a/src/com/owncloud/android/operations/RefreshFolderOperation.java b/src/com/owncloud/android/operations/RefreshFolderOperation.java new file mode 100644 index 00000000..8d6cfbb5 --- /dev/null +++ b/src/com/owncloud/android/operations/RefreshFolderOperation.java @@ -0,0 +1,611 @@ +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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 . + * + */ + +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. + */ +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 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 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(); + mRemoteFolderChanged = false; + mIgnoreETag = ignoreETag; + } + + + public int getConflictsFound() { + return mConflictsFound; + } + + public int getFailsInFavouritesFound() { + return mFailsInFavouritesFound; + } + + public Map 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 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 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 updatedFiles = new Vector(folderAndFiles.size() - 1); + List filesToSyncContents = new Vector(); + + // get current data about local contents of the folder to synchronize + List localFiles = mStorageManager.getFolderContent(mLocalFolder); + Map localFilesMap = new HashMap(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 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 shares = new ArrayList(); + 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; + } + +} diff --git a/src/com/owncloud/android/operations/RemoveFileOperation.java b/src/com/owncloud/android/operations/RemoveFileOperation.java index 6bd4e8a0..8f7067df 100644 --- a/src/com/owncloud/android/operations/RemoveFileOperation.java +++ b/src/com/owncloud/android/operations/RemoveFileOperation.java @@ -1,5 +1,9 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * @author masensio + * 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, @@ -27,9 +31,6 @@ import com.owncloud.android.operations.common.SyncOperation; /** * Remote operation performing the removal of a remote file or folder in the ownCloud server. - * - * @author David A. Velasco - * @author masensio */ public class RemoveFileOperation extends SyncOperation { diff --git a/src/com/owncloud/android/operations/RenameFileOperation.java b/src/com/owncloud/android/operations/RenameFileOperation.java index bd60e1fb..9726395a 100644 --- a/src/com/owncloud/android/operations/RenameFileOperation.java +++ b/src/com/owncloud/android/operations/RenameFileOperation.java @@ -1,5 +1,9 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * @author masensio + * 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, @@ -32,9 +36,6 @@ import com.owncloud.android.utils.FileStorageUtils; /** * Remote operation performing the rename of a remote file (or folder?) in the ownCloud server. - * - * @author David A. Velasco - * @author masensio */ public class RenameFileOperation extends SyncOperation { @@ -51,7 +52,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 +117,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 +129,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); } @@ -158,7 +158,7 @@ public class RenameFileOperation extends SyncOperation { */ private boolean isValidNewName() throws IOException { // check tricky names - if (mNewName == null || mNewName.length() <= 0 || mNewName.contains(File.separator) || mNewName.contains("%")) { + if (mNewName == null || mNewName.length() <= 0 || mNewName.contains(File.separator)) { return false; } // create a test file diff --git a/src/com/owncloud/android/operations/SynchronizeFileOperation.java b/src/com/owncloud/android/operations/SynchronizeFileOperation.java index 45a73057..6f1730e8 100644 --- a/src/com/owncloud/android/operations/SynchronizeFileOperation.java +++ b/src/com/owncloud/android/operations/SynchronizeFileOperation.java @@ -1,6 +1,10 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author David A. Velasco + * @author masensio * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2014 ownCloud Inc. + * 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, @@ -36,9 +40,6 @@ import android.content.Intent; /** * Remote operation performing the read of remote file in the ownCloud server. - * - * @author David A. Velasco - * @author masensio */ public class SynchronizeFileOperation extends SyncOperation { @@ -54,13 +55,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 +89,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 +119,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 +203,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 +219,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 +255,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; } diff --git a/src/com/owncloud/android/operations/SynchronizeFolderOperation.java b/src/com/owncloud/android/operations/SynchronizeFolderOperation.java index d61e6784..e80b42fa 100644 --- a/src/com/owncloud/android/operations/SynchronizeFolderOperation.java +++ b/src/com/owncloud/android/operations/SynchronizeFolderOperation.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -17,43 +20,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; /** @@ -64,228 +59,178 @@ import com.owncloud.android.utils.FileStorageUtils; * 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 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 mChildren; + //private List 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 mForgottenLocalFiles; - - /** 'True' means that this operation is part of a full account synchronization */ - private boolean mSyncFullAccount; + private int mFailsInFileSyncsFound; - /** '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 mFilesForDirectDownload; + // to avoid extra PROPFINDs when there was no change in the folder + private List mFilesToSyncContentsWithoutUpload; + // this will go out when 'folder synchronization' replaces 'folder download'; step by step + + private List 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(); mRemoteFolderChanged = false; - mIgnoreETag = ignoreETag; + mFilesForDirectDownload = new Vector(); + mFilesToSyncContentsWithoutUpload = new Vector(); + mFavouriteFilesToSyncContents = new Vector(); + mCancellationRequested = new AtomicBoolean(false); } - - + + public int getConflictsFound() { return mConflictsFound; } - - public int getFailsInFavouritesFound() { - return mFailsInFavouritesFound; - } - - public Map 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 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 +238,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 +259,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 folderAndFiles, OwnCloudClient client) { - // get 'fresh data' from the database - mLocalFolder = mStorageManager.getFileByPath(mLocalFolder.getRemotePath()); - - // parse data from remote folder + private void synchronizeData(ArrayList 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 updatedFiles = new Vector(folderAndFiles.size() - 1); - List filesToSyncContents = new Vector(); + mFilesForDirectDownload.clear(); + mFilesToSyncContentsWithoutUpload.clear(); + mFavouriteFilesToSyncContents.clear(); + + if (mCancellationRequested.get()) { + throw new OperationCancelledException(); + } // get current data about local contents of the folder to synchronize - List localFiles = mStorageManager.getFolderContent(mLocalFolder); + List localFiles = storageManager.getFolderContent(mLocalFolder); Map localFilesMap = new HashMap(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 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()); + } + + } else { + /// prepare limited synchronization for regular files + if (!child.isDown()) { + mFilesForDirectDownload.add(child); + } + } + } + } + - // request for the synchronization of file contents AFTER saving current remote properties - startContentSynchronizations(filesToSyncContents, client); + private void syncContents(OwnCloudClient client) throws OperationCancelledException { + startDirectDownloads(); + startContentSynchronizations(mFilesToSyncContentsWithoutUpload, client); + startContentSynchronizations(mFavouriteFilesToSyncContents, client); + } - mChildren = updatedFiles; + + 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 filesToSyncContents, OwnCloudClient client - ) { + private void startContentSynchronizations(List filesToSyncContents, OwnCloudClient client) + throws OperationCancelledException { + + Log_OC.v(TAG, "Starting content synchronization... "); 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 +485,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 shares = new ArrayList(); - 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 +506,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; + } } diff --git a/src/com/owncloud/android/operations/UnshareLinkOperation.java b/src/com/owncloud/android/operations/UnshareLinkOperation.java index c08c8e56..79370b74 100644 --- a/src/com/owncloud/android/operations/UnshareLinkOperation.java +++ b/src/com/owncloud/android/operations/UnshareLinkOperation.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author masensio + * 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, @@ -35,8 +38,6 @@ import com.owncloud.android.operations.common.SyncOperation; /** * Unshare file/folder * Save the data in Database - * - * @author masensio */ public class UnshareLinkOperation extends SyncOperation { diff --git a/src/com/owncloud/android/operations/UpdateOCVersionOperation.java b/src/com/owncloud/android/operations/UpdateOCVersionOperation.java index ac73d966..d2bf5f33 100644 --- a/src/com/owncloud/android/operations/UpdateOCVersionOperation.java +++ b/src/com/owncloud/android/operations/UpdateOCVersionOperation.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -38,8 +41,6 @@ import android.content.Context; /** * Remote operation that checks the version of an ownCloud server and stores it locally - * - * @author David A. Velasco */ public class UpdateOCVersionOperation extends RemoteOperation { diff --git a/src/com/owncloud/android/operations/UploadFileOperation.java b/src/com/owncloud/android/operations/UploadFileOperation.java index 1536a604..cd5f8a13 100644 --- a/src/com/owncloud/android/operations/UploadFileOperation.java +++ b/src/com/owncloud/android/operations/UploadFileOperation.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -26,6 +29,7 @@ import java.io.OutputStream; import java.util.HashSet; import java.util.Iterator; import java.util.Set; +import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.httpclient.methods.PutMethod; @@ -55,8 +59,6 @@ import com.owncloud.android.utils.UriUtils; /** * Remote operation performing the upload of a file to an ownCloud server - * - * @author David A. Velasco */ public class UploadFileOperation extends RemoteOperation { @@ -76,7 +78,7 @@ public class UploadFileOperation extends RemoteOperation { private String mOriginalStoragePath = null; PutMethod mPutMethod = null; private Set mDataTransferListeners = new HashSet(); - private final AtomicBoolean mCancellationRequested = new AtomicBoolean(false); + private AtomicBoolean mCancellationRequested = new AtomicBoolean(false); private Context mContext; private UploadRemoteFileOperation mUploadOperation; @@ -212,7 +214,8 @@ public class UploadFileOperation extends RemoteOperation { // check location of local file; if not the expected, copy to a // temporal file before upload (if COPY is the expected behaviour) - if (!mOriginalStoragePath.equals(expectedPath) && mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_COPY) { + if (!mOriginalStoragePath.equals(expectedPath) && + mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_COPY) { if (FileStorageUtils.getUsableSpace(mAccount.name) < originalFile.length()) { result = new RemoteOperationResult(ResultCode.LOCAL_STORAGE_FULL); @@ -221,7 +224,8 @@ public class UploadFileOperation extends RemoteOperation { } else { - String temporalPath = FileStorageUtils.getTemporalPath(mAccount.name) + mFile.getRemotePath(); + String temporalPath = FileStorageUtils.getTemporalPath(mAccount.name) + + mFile.getRemotePath(); mFile.setStoragePath(temporalPath); temporalFile = new File(temporalPath); @@ -251,10 +255,10 @@ public class UploadFileOperation extends RemoteOperation { int nRead; byte[] data = new byte[16384]; - while ((nRead = in.read(data, 0, data.length)) != -1) { + while (!mCancellationRequested.get() && + (nRead = in.read(data, 0, data.length)) != -1) { out.write(data, 0, nRead); } - out.flush(); } else { @@ -268,12 +272,17 @@ public class UploadFileOperation extends RemoteOperation { out = new FileOutputStream(temporalFile); byte[] buf = new byte[1024]; int len; - while ((len = in.read(buf)) > 0) { + while (!mCancellationRequested.get() && (len = in.read(buf)) > 0) { out.write(buf, 0, len); } } } + if (mCancellationRequested.get()) { + result = new RemoteOperationResult(new OperationCancelledException()); + } + + } catch (Exception e) { result = new RemoteOperationResult(ResultCode.LOCAL_STORAGE_NOT_COPIED); return result; @@ -283,63 +292,69 @@ public class UploadFileOperation extends RemoteOperation { if (in != null) in.close(); } catch (Exception e) { - Log_OC.d(TAG, "Weird exception while closing input stream for " + mOriginalStoragePath + " (ignoring)", e); + Log_OC.d(TAG, "Weird exception while closing input stream for " + + mOriginalStoragePath + " (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); + Log_OC.d(TAG, "Weird exception while closing output stream for " + + expectedPath + " (ignoring)", e); } } } } - localCopyPassed = true; + localCopyPassed = (result == null); /// perform the upload - if ( mChunked && (new File(mFile.getStoragePath())).length() > ChunkedUploadRemoteFileOperation.CHUNK_SIZE ) { - mUploadOperation = new ChunkedUploadRemoteFileOperation(mFile.getStoragePath(), mFile.getRemotePath(), - mFile.getMimetype()); + if ( mChunked && + (new File(mFile.getStoragePath())).length() > + ChunkedUploadRemoteFileOperation.CHUNK_SIZE ) { + mUploadOperation = new ChunkedUploadRemoteFileOperation(mFile.getStoragePath(), + mFile.getRemotePath(), mFile.getMimetype()); } else { - mUploadOperation = new UploadRemoteFileOperation(mFile.getStoragePath(), mFile.getRemotePath(), - mFile.getMimetype()); + mUploadOperation = new UploadRemoteFileOperation(mFile.getStoragePath(), + mFile.getRemotePath(), mFile.getMimetype()); } Iterator listener = mDataTransferListeners.iterator(); while (listener.hasNext()) { mUploadOperation.addDatatransferProgressListener(listener.next()); } - result = mUploadOperation.execute(client); - - /// move local temporal file or original file to its corresponding - // location in the ownCloud local folder - if (result.isSuccess()) { - if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_FORGET) { - mFile.setStoragePath(null); - - } else { - mFile.setStoragePath(expectedPath); - File fileToMove = null; - if (temporalFile != null) { // FileUploader.LOCAL_BEHAVIOUR_COPY - // ; see where temporalFile was - // set - fileToMove = temporalFile; - } else { // FileUploader.LOCAL_BEHAVIOUR_MOVE - fileToMove = originalFile; - } - if (!expectedFile.equals(fileToMove)) { - File expectedFolder = expectedFile.getParentFile(); - expectedFolder.mkdirs(); - if (!expectedFolder.isDirectory() || !fileToMove.renameTo(expectedFile)) { - mFile.setStoragePath(null); // forget the local file - // by now, treat this as a success; the file was - // uploaded; the user won't like that the local file - // is not linked, but this should be a very rare - // fail; - // the best option could be show a warning message - // (but not a fail) - // result = new - // RemoteOperationResult(ResultCode.LOCAL_STORAGE_NOT_MOVED); - // return result; + if (!mCancellationRequested.get()) { + result = mUploadOperation.execute(client); + + /// move local temporal file or original file to its corresponding + // location in the ownCloud local folder + if (result.isSuccess()) { + if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_FORGET) { + mFile.setStoragePath(null); + + } else { + mFile.setStoragePath(expectedPath); + File fileToMove = null; + if (temporalFile != null) { // FileUploader.LOCAL_BEHAVIOUR_COPY + // ; see where temporalFile was + // set + fileToMove = temporalFile; + } else { // FileUploader.LOCAL_BEHAVIOUR_MOVE + fileToMove = originalFile; + } + if (!expectedFile.equals(fileToMove)) { + File expectedFolder = expectedFile.getParentFile(); + expectedFolder.mkdirs(); + if (!expectedFolder.isDirectory() || !fileToMove.renameTo(expectedFile)) { + mFile.setStoragePath(null); // forget the local file + // by now, treat this as a success; the file was + // uploaded; the user won't like that the local file + // is not linked, but this should be a very rare + // fail; + // the best option could be show a warning message + // (but not a fail) + // result = new + // RemoteOperationResult(ResultCode.LOCAL_STORAGE_NOT_MOVED); + // return result; + } } } } @@ -358,19 +373,23 @@ public class UploadFileOperation extends RemoteOperation { temporalFile.delete(); } if (result.isSuccess()) { - Log_OC.i(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath + ": " + result.getLogMessage()); + Log_OC.i(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath + ": " + + result.getLogMessage()); } else { if (result.getException() != null) { String complement = ""; if (!nameCheckPassed) { complement = " (while checking file existence in server)"; } else if (!localCopyPassed) { - complement = " (while copying local file to " + FileStorageUtils.getSavePath(mAccount.name) + complement = " (while copying local file to " + + FileStorageUtils.getSavePath(mAccount.name) + ")"; } - Log_OC.e(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath + ": " + result.getLogMessage() + complement, result.getException()); + Log_OC.e(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath + + ": " + result.getLogMessage() + complement, result.getException()); } else { - Log_OC.e(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath + ": " + result.getLogMessage()); + Log_OC.e(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath + + ": " + result.getLogMessage()); } } } @@ -385,7 +404,8 @@ public class UploadFileOperation extends RemoteOperation { newFile.setFileLength(mFile.getFileLength()); newFile.setMimetype(mFile.getMimetype()); newFile.setModificationTimestamp(mFile.getModificationTimestamp()); - newFile.setModificationTimestampAtLastSyncForData(mFile.getModificationTimestampAtLastSyncForData()); + newFile.setModificationTimestampAtLastSyncForData( + mFile.getModificationTimestampAtLastSyncForData()); // newFile.setEtag(mFile.getEtag()) newFile.setKeepInSync(mFile.keepInSync()); newFile.setLastSyncDateForProperties(mFile.getLastSyncDateForProperties()); @@ -400,7 +420,8 @@ public class UploadFileOperation extends RemoteOperation { * Checks if remotePath does not exist in the server and returns it, or adds * a suffix to it in order to avoid the server file is overwritten. * - * @param string + * @param wc + * @param remotePath * @return */ private String getAvailableRemotePath(OwnCloudClient wc, String remotePath) throws Exception { @@ -436,12 +457,16 @@ public class UploadFileOperation extends RemoteOperation { } private boolean existsFile(OwnCloudClient client, String remotePath){ - ExistenceCheckRemoteOperation existsOperation = new ExistenceCheckRemoteOperation(remotePath, mContext, false); + ExistenceCheckRemoteOperation existsOperation = + new ExistenceCheckRemoteOperation(remotePath, mContext, false); RemoteOperationResult result = existsOperation.execute(client); return result.isSuccess(); } public void cancel() { - mUploadOperation.cancel(); + mCancellationRequested = new AtomicBoolean(true); + if (mUploadOperation != null) { + mUploadOperation.cancel(); + } } } diff --git a/src/com/owncloud/android/operations/common/SyncOperation.java b/src/com/owncloud/android/operations/common/SyncOperation.java index 8c5678b1..512be4e0 100644 --- a/src/com/owncloud/android/operations/common/SyncOperation.java +++ b/src/com/owncloud/android/operations/common/SyncOperation.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -32,8 +35,6 @@ import android.os.Handler; * with local data in the device. * * Provides methods to execute the operation both synchronously or asynchronously. - * - * @author David A. Velasco */ public abstract class SyncOperation extends RemoteOperation { diff --git a/src/com/owncloud/android/providers/FileContentProvider.java b/src/com/owncloud/android/providers/FileContentProvider.java index 21a8e2c9..737c6646 100644 --- a/src/com/owncloud/android/providers/FileContentProvider.java +++ b/src/com/owncloud/android/providers/FileContentProvider.java @@ -1,6 +1,10 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski + * @author David A. Velasco * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -45,10 +49,6 @@ import android.text.TextUtils; /** * The ContentProvider for the ownCloud App. - * - * @author Bartek Przybylski - * @author David A. Velasco - * */ public class FileContentProvider extends ContentProvider { @@ -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); } } diff --git a/src/com/owncloud/android/services/OperationsService.java b/src/com/owncloud/android/services/OperationsService.java index 16c4dcca..a4bc8f6c 100644 --- a/src/com/owncloud/android/services/OperationsService.java +++ b/src/com/owncloud/android/services/OperationsService.java @@ -1,5 +1,7 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * 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, @@ -26,6 +28,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; @@ -36,7 +39,6 @@ import com.owncloud.android.lib.common.operations.OnRemoteOperationListener; import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation; import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.lib.resources.users.GetRemoteUserNameOperation; import com.owncloud.android.operations.common.SyncOperation; @@ -48,6 +50,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,31 +84,25 @@ 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"; - - // 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"; - public static final String EXTRA_PASSWORD = "PASSWORD"; - public static final String EXTRA_AUTH_TOKEN = "AUTH_TOKEN"; + public static final String EXTRA_FILE = "FILE"; + public static final String EXTRA_COOKIE = "COOKIE"; public static final String ACTION_CREATE_SHARE = "CREATE_SHARE"; public static final String ACTION_UNSHARE = "UNSHARE"; public static final String ACTION_GET_SERVER_INFO = "GET_SERVER_INFO"; public static final String ACTION_OAUTH2_GET_ACCESS_TOKEN = "OAUTH2_GET_ACCESS_TOKEN"; - public static final String ACTION_EXISTENCE_CHECK = "EXISTENCE_CHECK"; public static final String ACTION_GET_USER_NAME = "GET_USER_NAME"; public static final String ACTION_RENAME = "RENAME"; 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_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> mPendingOperations = - new ConcurrentLinkedQueue>(); private ConcurrentMap> mUndispatchedFinishedOperations = @@ -114,30 +111,19 @@ public class OperationsService extends Service { private static class Target { public Uri mServerUrl = null; public Account mAccount = null; - public String mUsername = null; - public String mPassword = null; - public String mAuthToken = null; public String mCookie = null; - public Target(Account account, Uri serverUrl, String username, String password, String authToken, - String cookie) { + public Target(Account account, Uri serverUrl, String cookie) { mAccount = account; mServerUrl = serverUrl; - mUsername = username; - mPassword = password; - mAuthToken = authToken; mCookie = cookie; } } - 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 +131,18 @@ public class OperationsService extends Service { @Override public void onCreate() { super.onCreate(); - HandlerThread thread = new HandlerThread("Operations service thread", Process.THREAD_PRIORITY_BACKGROUND); + Log_OC.d(TAG, "Creating service"); + + /// First worker thread for most of operations + HandlerThread thread = new HandlerThread("Operations thread", Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + 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(); - mServiceLooper = thread.getLooper(); - mServiceHandler = new ServiceHandler(mServiceLooper, this); - mBinder = new OperationsServiceBinder(); + mSyncFolderHandler = new SyncFolderHandler(thread.getLooper(), this); } @@ -158,23 +151,45 @@ 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" ); + Log_OC.d(TAG, "Starting command with id " + startId); + + // WIP: for the moment, only SYNC_FOLDER is expected here; + // the rest of the operations are requested through the Binder + if (ACTION_SYNC_FOLDER.equals(intent.getAction())) { + + 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 itemSyncKey = new Pair(account, remotePath); + + Pair itemToQueue = newOperation(intent); + if (itemToQueue != null) { + mSyncFolderHandler.add(account, remotePath, (SynchronizeFolderOperation)itemToQueue.second); + Message msg = mSyncFolderHandler.obtainMessage(); + msg.arg1 = startId; + msg.obj = itemSyncKey; + mSyncFolderHandler.sendMessage(msg); + } + + } else { + Message msg = mOperationsHandler.obtainMessage(); + msg.arg1 = startId; + mOperationsHandler.sendMessage(msg); + } + return START_NOT_STICKY; } @Override public void onDestroy() { - //Log_OC.wtf(TAG, "onDestroy init" ); + Log_OC.v(TAG, "Destroying service" ); // Saving cookies try { OwnCloudClientManagerFactory.getDefaultSingleton(). @@ -191,14 +206,19 @@ public class OperationsService extends Service { e.printStackTrace(); } - //Log_OC.wtf(TAG, "Clear mUndispatchedFinisiedOperations" ); mUndispatchedFinishedOperations.clear(); - - //Log_OC.wtf(TAG, "onDestroy end" ); + + mOperationsBinder = null; + + mOperationsHandler.getLooper().quit(); + mOperationsHandler = null; + + mSyncFolderHandler.getLooper().quit(); + mSyncFolderHandler = null; + 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 +226,7 @@ public class OperationsService extends Service { @Override public IBinder onBind(Intent intent) { //Log_OC.wtf(TAG, "onBind" ); - return mBinder; + return mOperationsBinder; } @@ -215,11 +235,11 @@ public class OperationsService extends Service { */ @Override public boolean onUnbind(Intent intent) { - ((OperationsServiceBinder)mBinder).clearListeners(); + mOperationsBinder.clearListeners(); return false; // not accepting rebinding (default behaviour) } - + /** * Binder to let client components to perform actions on the queue of operations. * @@ -233,16 +253,24 @@ public class OperationsService extends Service { private ConcurrentMap mBoundListeners = new ConcurrentHashMap(); + 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) { + mSyncFolderHandler.cancel(account, file); } - - + + public void clearListeners() { mBoundListeners.clear(); @@ -280,131 +308,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(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, operation)); + public long queueNewOperation(Intent operationIntent) { + Pair 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 undispatched = mUndispatchedFinishedOperations.remove(operationId); @@ -413,7 +341,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 +349,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> mPendingOperations = + new ConcurrentLinkedQueue>(); + 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 +399,221 @@ public class OperationsService extends Service { @Override public void handleMessage(Message msg) { - mService.nextOperation(); + nextOperation(); + Log_OC.d(TAG, "Stopping after command with id " + msg.arg1); mService.stopSelf(msg.arg1); } - } - - - /** - * Performs the next operation in the queue - */ - private void nextOperation() { - //Log_OC.wtf(TAG, "nextOperation init" ); - Pair 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 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.mCookie != null && + mLastTarget.mCookie.length() > 0) { + // just used for GetUserName + // TODO refactor to run GetUserName as AsyncTask in the context of AuthenticatorActivity + 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(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 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 cookie = operationIntent.getStringExtra(EXTRA_COOKIE); + target = new Target( + account, + (serverUrl == null) ? null : Uri.parse(serverUrl), + 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_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, operation); + } else { + return null; + } + } + /** * Sends a broadcast when a new operation is added to the queue. @@ -593,18 +663,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( - Target target, final RemoteOperation operation, final RemoteOperationResult result) { + protected void dispatchResultToOperationListeners( + final RemoteOperation operation, final RemoteOperationResult result + ) { int count = 0; - Iterator listeners = mBinder.mBoundListeners.keySet().iterator(); + Iterator 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 +693,4 @@ public class OperationsService extends Service { } Log_OC.d(TAG, "Called " + count + " listeners"); } - - } diff --git a/src/com/owncloud/android/services/SyncFolderHandler.java b/src/com/owncloud/android/services/SyncFolderHandler.java new file mode 100644 index 00000000..57271eb5 --- /dev/null +++ b/src/com/owncloud/android/services/SyncFolderHandler.java @@ -0,0 +1,198 @@ +/** + * 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 . + * + */ + +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 mPendingOperations = + new IndexedForest(); + + 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 itemSyncKey = (Pair) msg.obj; + doOperation(itemSyncKey.first, itemSyncKey.second); + Log_OC.d(TAG, "Stopping after command with id " + msg.arg1); + mService.stopSelf(msg.arg1); + } + + + /** + * Performs the next operation in the queue + */ + private void doOperation(Account account, String 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); + + 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 { + mPendingOperations.removePayload(account, remotePath); + + mService.dispatchResultToOperationListeners(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; + } + Pair removeResult = + mPendingOperations.remove(account, file.getRemotePath()); + SynchronizeFolderOperation synchronization = removeResult.first; + if (synchronization != null) { + synchronization.cancel(); + } else { + // TODO synchronize? + if (mCurrentSyncOperation != null && mCurrentAccount != null && + mCurrentSyncOperation.getRemotePath().startsWith(file.getRemotePath()) && + account.name.equals(mCurrentAccount.name)) { + mCurrentSyncOperation.cancel(); + } + } + + //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); + } + + +} diff --git a/src/com/owncloud/android/services/observer/FileObserverService.java b/src/com/owncloud/android/services/observer/FileObserverService.java index 114f0e42..83de450b 100644 --- a/src/com/owncloud/android/services/observer/FileObserverService.java +++ b/src/com/owncloud/android/services/observer/FileObserverService.java @@ -1,6 +1,9 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author David A. Velasco * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2014 ownCloud Inc. + * 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, @@ -54,8 +57,6 @@ import com.owncloud.android.utils.FileStorageUtils; * memory. To minimize the impact of this, the service always returns * Service.START_STICKY, and the later restart of the service is explicitly * considered in {@link FileObserverService#onStartCommand(Intent, int, int)}. - * - * @author David A. Velasco */ public class FileObserverService extends Service { diff --git a/src/com/owncloud/android/services/observer/FolderObserver.java b/src/com/owncloud/android/services/observer/FolderObserver.java index 67b41a13..5329b526 100644 --- a/src/com/owncloud/android/services/observer/FolderObserver.java +++ b/src/com/owncloud/android/services/observer/FolderObserver.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -46,8 +49,6 @@ import com.owncloud.android.ui.activity.ConflictsResolveActivity; * The second case requires to monitor the folder parent of the files, since a direct * {@link FileObserver} on it will not receive more events after the file is deleted to * be replaced. - * - * @author David A. Velasco */ public class FolderObserver extends FileObserver { diff --git a/src/com/owncloud/android/syncadapter/AbstractOwnCloudSyncAdapter.java b/src/com/owncloud/android/syncadapter/AbstractOwnCloudSyncAdapter.java index 28cfa54c..7ccbc118 100644 --- a/src/com/owncloud/android/syncadapter/AbstractOwnCloudSyncAdapter.java +++ b/src/com/owncloud/android/syncadapter/AbstractOwnCloudSyncAdapter.java @@ -1,6 +1,10 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author sassman + * @author David A. Velasco * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -44,9 +48,6 @@ import android.content.Context; * resource types, like FileSync, ConcatsSync, CalendarSync, etc.. * * Implements the standard {@link AbstractThreadedSyncAdapter}. - * - * @author sassman - * @author David A. Velasco */ public abstract class AbstractOwnCloudSyncAdapter extends AbstractThreadedSyncAdapter { diff --git a/src/com/owncloud/android/syncadapter/ContactSyncAdapter.java b/src/com/owncloud/android/syncadapter/ContactSyncAdapter.java index 3ba1676a..d3ab06c1 100644 --- a/src/com/owncloud/android/syncadapter/ContactSyncAdapter.java +++ b/src/com/owncloud/android/syncadapter/ContactSyncAdapter.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, diff --git a/src/com/owncloud/android/syncadapter/ContactSyncService.java b/src/com/owncloud/android/syncadapter/ContactSyncService.java index 6d7c46c0..d907bb49 100644 --- a/src/com/owncloud/android/syncadapter/ContactSyncService.java +++ b/src/com/owncloud/android/syncadapter/ContactSyncService.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, diff --git a/src/com/owncloud/android/syncadapter/FileSyncAdapter.java b/src/com/owncloud/android/syncadapter/FileSyncAdapter.java index 33e24003..07e68c6a 100644 --- a/src/com/owncloud/android/syncadapter/FileSyncAdapter.java +++ b/src/com/owncloud/android/syncadapter/FileSyncAdapter.java @@ -1,6 +1,10 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski + * @author David A. Velasco * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -31,7 +35,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; @@ -55,9 +59,6 @@ import android.support.v4.app.NotificationCompat; * ownCloud files. * * Performs a full synchronization of the account recieved in {@link #onPerformSync(Account, Bundle, String, ContentProviderClient, SyncResult)}. - * - * @author Bartek Przybylski - * @author David A. Velasco */ public class FileSyncAdapter extends AbstractOwnCloudSyncAdapter { @@ -260,7 +261,7 @@ public class FileSyncAdapter extends AbstractOwnCloudSyncAdapter { } */ // folder synchronization - SynchronizeFolderOperation synchFolderOp = new SynchronizeFolderOperation( folder, + RefreshFolderOperation synchFolderOp = new RefreshFolderOperation( folder, mCurrentSyncTime, true, mIsShareSupported, diff --git a/src/com/owncloud/android/syncadapter/FileSyncService.java b/src/com/owncloud/android/syncadapter/FileSyncService.java index 5da8c24c..e48f91fd 100644 --- a/src/com/owncloud/android/syncadapter/FileSyncService.java +++ b/src/com/owncloud/android/syncadapter/FileSyncService.java @@ -1,6 +1,10 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski + * @author David A. Velasco * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -24,10 +28,7 @@ import android.os.IBinder; /** * Background service for synchronizing remote files with their local state. * - * Serves as a connector to an instance of {@link FileSyncAdapter}, as required by standard Android APIs. - * - * @author Bartek Przybylski - * @author David A. Velasco + * Serves as a connector to an instance of {@link FileSyncAdapter}, as required by standard Android APIs. */ public class FileSyncService extends Service { diff --git a/src/com/owncloud/android/ui/ActionItem.java b/src/com/owncloud/android/ui/ActionItem.java index a65f3ad0..e1fa805c 100644 --- a/src/com/owncloud/android/ui/ActionItem.java +++ b/src/com/owncloud/android/ui/ActionItem.java @@ -1,6 +1,9 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -22,9 +25,6 @@ import android.view.View.OnClickListener; /** * Represents an Item on the ActionBar. - * - * @author Bartek Przybylski - * */ public class ActionItem { private Drawable mIcon; diff --git a/src/com/owncloud/android/ui/CheckBoxPreferenceWithLongTitle.java b/src/com/owncloud/android/ui/CheckBoxPreferenceWithLongTitle.java index dac083af..b451fca3 100644 --- a/src/com/owncloud/android/ui/CheckBoxPreferenceWithLongTitle.java +++ b/src/com/owncloud/android/ui/CheckBoxPreferenceWithLongTitle.java @@ -1,4 +1,6 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2014 ownCloud Inc. * * This program is free software: you can redistribute it and/or modify diff --git a/src/com/owncloud/android/ui/CustomPopup.java b/src/com/owncloud/android/ui/CustomPopup.java index fccf56d2..44f8976f 100644 --- a/src/com/owncloud/android/ui/CustomPopup.java +++ b/src/com/owncloud/android/ui/CustomPopup.java @@ -1,6 +1,9 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Lorensius. W. T * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -32,9 +35,6 @@ import android.widget.PopupWindow; /** * Represents a custom PopupWindows - * - * @author Lorensius. W. T - * */ public class CustomPopup { protected final View mAnchor; diff --git a/src/com/owncloud/android/ui/ExtendedListView.java b/src/com/owncloud/android/ui/ExtendedListView.java index 9fe885bf..22587643 100644 --- a/src/com/owncloud/android/ui/ExtendedListView.java +++ b/src/com/owncloud/android/ui/ExtendedListView.java @@ -1,6 +1,9 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author David A. Velasco * 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, @@ -23,16 +26,18 @@ import android.graphics.Canvas; import android.util.AttributeSet; import android.widget.ListView; +import com.owncloud.android.lib.common.utils.Log_OC; + /** * ListView allowing to specify the position of an item that should be centered in the visible area, if possible. - * - * The cleanest way I found to overcome the problem due to getHeight() returns 0 until the view is really drawn. - * - * @author David A. Velasco + * + * The cleanest way I found to overcome the problem due to getHeight() returns 0 until the view is really drawn. */ public class ExtendedListView extends ListView { - private int mPositionToSetAndCenter; + private static final String TAG = ExtendedListView.class.getSimpleName(); + + private int mPositionToSetAndCenter = 0; public ExtendedListView(Context context) { super(context); @@ -48,26 +53,28 @@ public class ExtendedListView extends ListView { /** * {@inheritDoc} - * - * + * + * */ @Override protected void onDraw (Canvas canvas) { super.onDraw(canvas); if (mPositionToSetAndCenter > 0) { + Log_OC.v(TAG, "Centering around position " + mPositionToSetAndCenter); this.setSelectionFromTop(mPositionToSetAndCenter, getHeight() / 2); mPositionToSetAndCenter = 0; } } - + /** * Public method to set the position of the item that should be centered in the visible area of the view. - * + * * The position is saved here and checked in onDraw(). - * + * * @param position Position (in the list of items) of the item to center in the visible area. */ public void setAndCenterSelection(int position) { mPositionToSetAndCenter = position; } -} + +} \ No newline at end of file diff --git a/src/com/owncloud/android/ui/PreferenceWithLongSummary.java b/src/com/owncloud/android/ui/PreferenceWithLongSummary.java index e38d29a2..946a41e0 100644 --- a/src/com/owncloud/android/ui/PreferenceWithLongSummary.java +++ b/src/com/owncloud/android/ui/PreferenceWithLongSummary.java @@ -1,4 +1,6 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2014 ownCloud Inc. * * This program is free software: you can redistribute it and/or modify diff --git a/src/com/owncloud/android/ui/QuickAction.java b/src/com/owncloud/android/ui/QuickAction.java index 86fe3fe3..db27951e 100644 --- a/src/com/owncloud/android/ui/QuickAction.java +++ b/src/com/owncloud/android/ui/QuickAction.java @@ -1,6 +1,9 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Lorensius. W. T * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -42,8 +45,6 @@ import com.owncloud.android.R; /** * Popup window, shows action list as icon and text like the one in Gallery3D * app. - * - * @author Lorensius. W. T */ public class QuickAction extends CustomPopup { private final View root; diff --git a/src/com/owncloud/android/ui/RadioButtonPreference.java b/src/com/owncloud/android/ui/RadioButtonPreference.java index 8f562b36..4437cce2 100644 --- a/src/com/owncloud/android/ui/RadioButtonPreference.java +++ b/src/com/owncloud/android/ui/RadioButtonPreference.java @@ -1,3 +1,22 @@ +/** + * 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 . + * + */ + package com.owncloud.android.ui; import android.content.Context; diff --git a/src/com/owncloud/android/ui/SquareImageView.java b/src/com/owncloud/android/ui/SquareImageView.java new file mode 100644 index 00000000..b1613fdc --- /dev/null +++ b/src/com/owncloud/android/ui/SquareImageView.java @@ -0,0 +1,44 @@ +/** + * 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 . + * + */ + +package com.owncloud.android.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +public class SquareImageView extends ImageView { + + public SquareImageView(Context context) { + super(context); + } + + public SquareImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SquareImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } +} diff --git a/src/com/owncloud/android/ui/SquareLinearLayout.java b/src/com/owncloud/android/ui/SquareLinearLayout.java new file mode 100644 index 00000000..c65c51f1 --- /dev/null +++ b/src/com/owncloud/android/ui/SquareLinearLayout.java @@ -0,0 +1,44 @@ +/** + * 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 . + * + */ + +package com.owncloud.android.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +public class SquareLinearLayout extends LinearLayout { + + public SquareLinearLayout(Context context) { + super(context); + } + + public SquareLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SquareLinearLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } +} diff --git a/src/com/owncloud/android/ui/activity/ComponentsGetter.java b/src/com/owncloud/android/ui/activity/ComponentsGetter.java index 076a6cba..043f67e1 100644 --- a/src/com/owncloud/android/ui/activity/ComponentsGetter.java +++ b/src/com/owncloud/android/ui/activity/ComponentsGetter.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -22,28 +24,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(); + } diff --git a/src/com/owncloud/android/ui/activity/ConflictsResolveActivity.java b/src/com/owncloud/android/ui/activity/ConflictsResolveActivity.java index 509e5c79..c3db9a47 100644 --- a/src/com/owncloud/android/ui/activity/ConflictsResolveActivity.java +++ b/src/com/owncloud/android/ui/activity/ConflictsResolveActivity.java @@ -1,6 +1,10 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski + * @author David A. Velasco * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -32,10 +36,7 @@ import android.os.Bundle; /** * Wrapper activity which will be launched if keep-in-sync file will be modified by external - * application. - * - * @author Bartek Przybylski - * @author David A. Velasco + * application. */ public class ConflictsResolveActivity extends FileActivity implements OnConflictDecisionMadeListener { diff --git a/src/com/owncloud/android/ui/activity/CopyToClipboardActivity.java b/src/com/owncloud/android/ui/activity/CopyToClipboardActivity.java index b503c379..de5b3c3e 100644 --- a/src/com/owncloud/android/ui/activity/CopyToClipboardActivity.java +++ b/src/com/owncloud/android/ui/activity/CopyToClipboardActivity.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -28,8 +31,6 @@ import android.widget.Toast; /** * Activity copying the text of the received Intent into the system clibpoard. - * - * @author David A. Velasco */ @SuppressWarnings("deprecation") public class CopyToClipboardActivity extends Activity { diff --git a/src/com/owncloud/android/ui/activity/ErrorsWhileCopyingHandlerActivity.java b/src/com/owncloud/android/ui/activity/ErrorsWhileCopyingHandlerActivity.java index bffec8b4..9405ca7d 100644 --- a/src/com/owncloud/android/ui/activity/ErrorsWhileCopyingHandlerActivity.java +++ b/src/com/owncloud/android/ui/activity/ErrorsWhileCopyingHandlerActivity.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -57,8 +60,6 @@ import com.owncloud.android.utils.FileStorageUtils; * files. * * Shown when the error notification summarizing the list of errors is clicked by the user. - * - * @author David A. Velasco */ public class ErrorsWhileCopyingHandlerActivity extends SherlockFragmentActivity implements OnClickListener { @@ -129,9 +130,7 @@ public class ErrorsWhileCopyingHandlerActivity extends SherlockFragmentActivity /** * Customized adapter, showing the local files as main text in two-lines list item and the remote files - * as the secondary text. - * - * @author David A. Velasco + * as the secondary text. */ public class ErrorsWhileCopyingListAdapter extends ArrayAdapter { @@ -200,8 +199,6 @@ public class ErrorsWhileCopyingHandlerActivity extends SherlockFragmentActivity /** * Asynchronous task performing the move of all the local files to the ownCloud folder. - * - * @author David A. Velasco */ private class MoveFilesTask extends AsyncTask { diff --git a/src/com/owncloud/android/ui/activity/FileActivity.java b/src/com/owncloud/android/ui/activity/FileActivity.java index 136bdb55..e92474b7 100644 --- a/src/com/owncloud/android/ui/activity/FileActivity.java +++ b/src/com/owncloud/android/ui/activity/FileActivity.java @@ -1,6 +1,9 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author David A. Velasco * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2014 ownCloud Inc. + * 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, @@ -54,8 +57,8 @@ 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; import com.owncloud.android.services.OperationsService.OperationsServiceBinder; import com.owncloud.android.ui.dialog.LoadingDialog; @@ -64,11 +67,9 @@ import com.owncloud.android.utils.ErrorMessageAdapter; /** * Activity with common behaviour for activities handling {@link OCFile}s in ownCloud {@link Account}s . - * - * @author David A. Velasco */ -public class FileActivity extends SherlockFragmentActivity -implements OnRemoteOperationListener, ComponentsGetter { +public class FileActivity extends SherlockFragmentActivity + implements OnRemoteOperationListener, ComponentsGetter { public static final String EXTRA_FILE = "com.owncloud.android.ui.activity.FILE"; public static final String EXTRA_ACCOUNT = "com.owncloud.android.ui.activity.ACCOUNT"; @@ -78,7 +79,7 @@ implements OnRemoteOperationListener, ComponentsGetter { public static final String TAG = FileActivity.class.getSimpleName(); private static final String DIALOG_WAIT_TAG = "DIALOG_WAIT"; - private static final String KEY_WAITING_FOR_OP_ID = "WAITING_FOR_OP_ID";; + private static final String KEY_WAITING_FOR_OP_ID = "WAITING_FOR_OP_ID"; protected static final long DELAY_TO_REQUEST_OPERATION_ON_ACTIVITY_RESULTS = 200; @@ -157,7 +158,7 @@ implements OnRemoteOperationListener, ComponentsGetter { if (mUploadServiceConnection != null) { bindService(new Intent(this, FileUploader.class), mUploadServiceConnection, Context.BIND_AUTO_CREATE); } - + } @@ -220,6 +221,7 @@ implements OnRemoteOperationListener, ComponentsGetter { unbindService(mUploadServiceConnection); mUploadServiceConnection = null; } + super.onDestroy(); } @@ -255,8 +257,6 @@ implements OnRemoteOperationListener, ComponentsGetter { * to create a new ownCloud {@link Account}. * * POSTCONDITION: updates {@link #mAccountWasSet} and {@link #mAccountWasRestored}. - * - * @return 'True' if the checked {@link Account} was valid. */ private void swapToDefaultAccount() { // default to the most recently used account @@ -355,15 +355,12 @@ implements OnRemoteOperationListener, ComponentsGetter { protected ServiceConnection newTransferenceServiceConnection() { return null; } - /** * Helper class handling a callback from the {@link AccountManager} after the creation of * a new ownCloud {@link Account} finished, successfully or not. * * At this moment, only called after the creation of the first account. - * - * @author David A. Velasco */ public class AccountCreationCallback implements AccountManagerCallback { @@ -464,7 +461,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 +506,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(); @@ -594,7 +601,7 @@ implements OnRemoteOperationListener, ComponentsGetter { @Override public FileUploaderBinder getFileUploaderBinder() { return mUploaderBinder; - }; + } } diff --git a/src/com/owncloud/android/ui/activity/FileDisplayActivity.java b/src/com/owncloud/android/ui/activity/FileDisplayActivity.java index ea65d31a..58e7ec5e 100644 --- a/src/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/src/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -1,6 +1,10 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski + * @author David A. Velasco * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2014 ownCloud Inc. + * 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, @@ -25,6 +29,8 @@ import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; @@ -90,11 +96,10 @@ 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; -import com.owncloud.android.ui.adapter.FileListListAdapter; import com.owncloud.android.ui.dialog.CreateFolderDialogFragment; import com.owncloud.android.ui.dialog.SslUntrustedCertDialog; import com.owncloud.android.ui.dialog.SslUntrustedCertDialog.OnSslUntrustedCertListener; @@ -107,14 +112,12 @@ import com.owncloud.android.ui.preview.PreviewMediaFragment; import com.owncloud.android.ui.preview.PreviewVideoActivity; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.ErrorMessageAdapter; +import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.UriUtils; /** * Displays, what files the user has available in his ownCloud. - * - * @author Bartek Przybylski - * @author David A. Velasco */ public class FileDisplayActivity extends HookActivity implements @@ -253,7 +256,7 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { setNavigationListWithFolder(file); if (!stateWasRecovered) { - Log_OC.e(TAG, "Initializing Fragments in onAccountChanged.."); + Log_OC.d(TAG, "Initializing Fragments in onAccountChanged.."); initFragmentsWithFile(); if (file.isFolder()) { startSyncFolderOperation(file, false); @@ -517,7 +520,7 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { // Read sorting order, default to sort by name ascending Integer sortOrder = appPreferences - .getInt("sortOrder", FileListListAdapter.SORT_NAME); + .getInt("sortOrder", FileStorageUtils.SORT_NAME); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.actionbar_sort_title) @@ -552,19 +555,19 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { } private void startSynchronization() { - Log_OC.e(TAG, "Got to start sync"); + Log_OC.d(TAG, "Got to start sync"); if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT) { - Log_OC.e(TAG, "Canceling all syncs for " + MainApp.getAuthority()); + Log_OC.d(TAG, "Canceling all syncs for " + MainApp.getAuthority()); ContentResolver.cancelSync(null, MainApp.getAuthority()); // cancel the current synchronizations of any ownCloud account Bundle bundle = new Bundle(); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); - Log_OC.e(TAG, "Requesting sync for " + getAccount().name + " at " + MainApp.getAuthority()); + Log_OC.d(TAG, "Requesting sync for " + getAccount().name + " at " + MainApp.getAuthority()); ContentResolver.requestSync( getAccount(), MainApp.getAuthority(), bundle); } else { - Log_OC.e(TAG, "Requesting sync for " + getAccount().name + " at " + MainApp.getAuthority() + " with new API"); + Log_OC.d(TAG, "Requesting sync for " + getAccount().name + " at " + MainApp.getAuthority() + " with new API"); SyncRequest.Builder builder = new SyncRequest.Builder(); builder.setSyncAdapter(getAccount(), MainApp.getAuthority()); builder.setExpedited(true); @@ -605,13 +608,23 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { /** * Called, when the user selected something for uploading + * */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == ACTION_SELECT_CONTENT_FROM_APPS && (resultCode == RESULT_OK || resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE)) { - requestSimpleUpload(data, resultCode); - + //getClipData is only supported on api level 16+, Jelly Bean + if (data.getData() == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN){ + for( int i = 0; i < data.getClipData().getItemCount(); i++){ + Intent intent = new Intent(); + intent.setData(data.getClipData().getItemAt(i).getUri()); + requestSimpleUpload(intent, resultCode); + } + }else { + requestSimpleUpload(data, resultCode); + } } else if (requestCode == ACTION_SELECT_MULTIPLE_FILES && (resultCode == RESULT_OK || resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE)) { requestMultipleUpload(data, resultCode); @@ -636,6 +649,7 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { if (filePaths != null) { String[] remotePaths = new String[filePaths.length]; String remotePathBase = ""; + for (int j = mDirectories.getCount() - 2; j >= 0; --j) { remotePathBase += OCFile.PATH_SEPARATOR + mDirectories.getItem(j); } @@ -686,7 +700,7 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { } finally { if (filepath == null) { - Log_OC.e(TAG, "Couldnt resolve path to file"); + Log_OC.e(TAG, "Couldn't resolve path to file"); Toast t = Toast.makeText(this, getString(R.string.filedisplay_unexpected_bad_get_content), Toast.LENGTH_LONG); t.show(); return; @@ -773,7 +787,7 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { @Override protected void onSaveInstanceState(Bundle outState) { // responsibility of restore is preferred in onCreate() before than in onRestoreInstanceState when there are Fragments involved - Log_OC.e(TAG, "onSaveInstanceState() start"); + Log_OC.d(TAG, "onSaveInstanceState() start"); super.onSaveInstanceState(outState); outState.putParcelable(FileDisplayActivity.KEY_WAITING_TO_PREVIEW, mWaitingToPreview); outState.putBoolean(FileDisplayActivity.KEY_SYNC_IN_PROGRESS, mSyncInProgress); @@ -788,7 +802,7 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { @Override protected void onResume() { super.onResume(); - Log_OC.e(TAG, "onResume() start"); + Log_OC.d(TAG, "onResume() start"); // refresh list of files refreshListOfFilesFragment(); @@ -797,8 +811,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); @@ -820,7 +834,7 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { @Override protected void onPause() { - Log_OC.e(TAG, "onPause() start"); + Log_OC.d(TAG, "onPause() start"); if (mSyncBroadcastReceiver != null) { unregisterReceiver(mSyncBroadcastReceiver); //LocalBroadcastManager.getInstance(this).unregisterReceiver(mSyncBroadcastReceiver); @@ -877,6 +891,10 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { } else if (item == 1) { Intent action = new Intent(Intent.ACTION_GET_CONTENT); action = action.setType("*/*").addCategory(Intent.CATEGORY_OPENABLE); + //Intent.EXTRA_ALLOW_MULTIPLE is only supported on api level 18+, Jelly Bean + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + action.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + } startActivityForResult(Intent.createChooser(action, getString(R.string.upload_chooser_title)), ACTION_SELECT_CONTENT_FROM_APPS); } @@ -1082,9 +1100,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() && @@ -1093,40 +1111,34 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { (synchResult.isException() && synchResult.getException() instanceof AuthenticatorException))) { - OwnCloudClient client = null; + try { - OwnCloudAccount ocAccount = + OwnCloudClient client; + OwnCloudAccount ocAccount = new OwnCloudAccount(getAccount(), context); client = (OwnCloudClientManagerFactory.getDefaultSingleton(). removeClientFor(ocAccount)); - // TODO get rid of these exceptions - } catch (AccountNotFoundException e) { - e.printStackTrace(); - } catch (AuthenticatorException e) { - e.printStackTrace(); - } catch (OperationCanceledException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - if (client != null) { - OwnCloudCredentials cred = client.getCredentials(); - if (cred != null) { - AccountManager am = AccountManager.get(context); - if (cred.authTokenExpires()) { - am.invalidateAuthToken( - getAccount().type, - cred.getAuthToken() - ); - } else { - am.clearPassword(getAccount()); + + if (client != null) { + OwnCloudCredentials cred = client.getCredentials(); + if (cred != null) { + AccountManager am = AccountManager.get(context); + if (cred.authTokenExpires()) { + am.invalidateAuthToken( + getAccount().type, + cred.getAuthToken() + ); + } else { + am.clearPassword(getAccount()); + } } } + requestCredentialsUpdate(); + + } catch (AccountNotFoundException e) { + Log_OC.e(TAG, "Account " + getAccount() + " was removed!", e); } - - requestCredentialsUpdate(); - + } } removeStickyBroadcast(intent); @@ -1238,26 +1250,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(); } @@ -1272,7 +1294,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) { @@ -1714,6 +1748,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); @@ -1742,7 +1777,7 @@ OnSslUntrustedCertListener, OnEnforceableRefreshListener { mSyncInProgress = true; // perform folder synchronization - RemoteOperation synchFolderOp = new SynchronizeFolderOperation( folder, + RemoteOperation synchFolderOp = new RefreshFolderOperation( folder, currentSyncTime, false, getFileOperationsHelper().isSharedSupported(), @@ -1771,7 +1806,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); diff --git a/src/com/owncloud/android/ui/activity/FolderPickerActivity.java b/src/com/owncloud/android/ui/activity/FolderPickerActivity.java index 07c92134..3a61eb06 100644 --- a/src/com/owncloud/android/ui/activity/FolderPickerActivity.java +++ b/src/com/owncloud/android/ui/activity/FolderPickerActivity.java @@ -1,5 +1,7 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * 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, @@ -55,7 +57,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 +210,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 +238,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 +480,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() && @@ -489,40 +491,33 @@ public class FolderPickerActivity extends FileActivity implements FileFragment.C (synchResult.isException() && synchResult.getException() instanceof AuthenticatorException))) { - OwnCloudClient client = null; try { - OwnCloudAccount ocAccount = + OwnCloudClient client; + OwnCloudAccount ocAccount = new OwnCloudAccount(getAccount(), context); client = (OwnCloudClientManagerFactory.getDefaultSingleton(). removeClientFor(ocAccount)); - // TODO get rid of these exceptions - } catch (AccountNotFoundException e) { - e.printStackTrace(); - } catch (AuthenticatorException e) { - e.printStackTrace(); - } catch (OperationCanceledException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - if (client != null) { - OwnCloudCredentials cred = client.getCredentials(); - if (cred != null) { - AccountManager am = AccountManager.get(context); - if (cred.authTokenExpires()) { - am.invalidateAuthToken( - getAccount().type, - cred.getAuthToken() - ); - } else { - am.clearPassword(getAccount()); + + if (client != null) { + OwnCloudCredentials cred = client.getCredentials(); + if (cred != null) { + AccountManager am = AccountManager.get(context); + if (cred.authTokenExpires()) { + am.invalidateAuthToken( + getAccount().type, + cred.getAuthToken() + ); + } else { + am.clearPassword(getAccount()); + } } } + requestCredentialsUpdate(); + + } catch (AccountNotFoundException e) { + Log_OC.e(TAG, "Account " + getAccount() + " was removed!", e); } - - requestCredentialsUpdate(); - + } } removeStickyBroadcast(intent); diff --git a/src/com/owncloud/android/ui/activity/GenericExplanationActivity.java b/src/com/owncloud/android/ui/activity/GenericExplanationActivity.java index 901434e2..0d4ab01c 100644 --- a/src/com/owncloud/android/ui/activity/GenericExplanationActivity.java +++ b/src/com/owncloud/android/ui/activity/GenericExplanationActivity.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -41,8 +44,6 @@ import com.owncloud.android.utils.DisplayUtils; * * Added to show explanations for notifications when the user clicks on them, and there no place * better to show them. - * - * @author David A. Velasco */ public class GenericExplanationActivity extends SherlockFragmentActivity { diff --git a/src/com/owncloud/android/ui/activity/HookActivity.java b/src/com/owncloud/android/ui/activity/HookActivity.java index 54d65b1b..daca8ad0 100644 --- a/src/com/owncloud/android/ui/activity/HookActivity.java +++ b/src/com/owncloud/android/ui/activity/HookActivity.java @@ -1,5 +1,7 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * 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, diff --git a/src/com/owncloud/android/ui/activity/LogHistoryActivity.java b/src/com/owncloud/android/ui/activity/LogHistoryActivity.java index 793b3d9d..5640404b 100644 --- a/src/com/owncloud/android/ui/activity/LogHistoryActivity.java +++ b/src/com/owncloud/android/ui/activity/LogHistoryActivity.java @@ -1,5 +1,7 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * 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, diff --git a/src/com/owncloud/android/ui/activity/OnEnforceableRefreshListener.java b/src/com/owncloud/android/ui/activity/OnEnforceableRefreshListener.java index 22bdb18b..d82f33b8 100644 --- a/src/com/owncloud/android/ui/activity/OnEnforceableRefreshListener.java +++ b/src/com/owncloud/android/ui/activity/OnEnforceableRefreshListener.java @@ -1,3 +1,24 @@ + +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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 . + * + */ + package com.owncloud.android.ui.activity; import android.support.v4.widget.SwipeRefreshLayout; diff --git a/src/com/owncloud/android/ui/activity/PinCodeActivity.java b/src/com/owncloud/android/ui/activity/PinCodeActivity.java index 39b973d0..76ece933 100644 --- a/src/com/owncloud/android/ui/activity/PinCodeActivity.java +++ b/src/com/owncloud/android/ui/activity/PinCodeActivity.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2011 Bartek Przybylski + * 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, diff --git a/src/com/owncloud/android/ui/activity/Preferences.java b/src/com/owncloud/android/ui/activity/Preferences.java index 7942a73f..2eb48fab 100644 --- a/src/com/owncloud/android/ui/activity/Preferences.java +++ b/src/com/owncloud/android/ui/activity/Preferences.java @@ -1,6 +1,10 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski + * @author David A. Velasco * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -21,13 +25,17 @@ import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountManagerCallback; import android.accounts.AccountManagerFuture; +import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; @@ -50,20 +58,25 @@ import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.authentication.AccountUtils; import com.owncloud.android.authentication.AuthenticatorActivity; +import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.db.DbHandler; +import com.owncloud.android.files.FileOperationsHelper; +import com.owncloud.android.files.services.FileDownloader; +import com.owncloud.android.files.services.FileUploader; import com.owncloud.android.lib.common.utils.Log_OC; +import com.owncloud.android.services.OperationsService; import com.owncloud.android.ui.RadioButtonPreference; import com.owncloud.android.utils.DisplayUtils; +import java.io.File; + /** * An Activity that allows the user to change the application's settings. - * - * @author Bartek Przybylski - * @author David A. Velasco */ -public class Preferences extends SherlockPreferenceActivity implements AccountManagerCallback { +public class Preferences extends SherlockPreferenceActivity + implements AccountManagerCallback, ComponentsGetter { private static final String TAG = "OwnCloudPreferences"; @@ -79,10 +92,18 @@ 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; + protected FileDownloader.FileDownloaderBinder mDownloaderBinder = null; + protected FileUploader.FileUploaderBinder mUploaderBinder = null; + private ServiceConnection mDownloadServiceConnection, mUploadServiceConnection = null; @SuppressWarnings("deprecation") @Override @@ -198,13 +219,13 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa String username = currentAccount.name.substring(0, currentAccount.name.lastIndexOf('@')); String recommendSubject = String.format(getString(R.string.recommend_subject), appName); - String recommendText = String.format(getString(R.string.recommend_text), appName, downloadUrl, username); + String recommendText = String.format(getString(R.string.recommend_text), + appName, downloadUrl, username); intent.putExtra(Intent.EXTRA_SUBJECT, recommendSubject); intent.putExtra(Intent.EXTRA_TEXT, recommendText); startActivity(intent); - return(true); } @@ -279,7 +300,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){ @@ -296,6 +333,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"); @@ -307,6 +357,38 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa loadInstantUploadPath(); loadInstantUploadVideoPath(); + /* ComponentsGetter */ + mDownloadServiceConnection = newTransferenceServiceConnection(); + if (mDownloadServiceConnection != null) { + bindService(new Intent(this, FileDownloader.class), mDownloadServiceConnection, + Context.BIND_AUTO_CREATE); + } + mUploadServiceConnection = newTransferenceServiceConnection(); + if (mUploadServiceConnection != null) { + bindService(new Intent(this, FileUploader.class), mUploadServiceConnection, + Context.BIND_AUTO_CREATE); + } + + } + + 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 @@ -352,6 +434,7 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa // Remove account am.removeAccount(a, this, mHandler); + Log_OC.d(TAG, "Remove an account " + a.name); } } } @@ -362,6 +445,18 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa @Override public void run(AccountManagerFuture future) { if (future.isDone()) { + // after remove account + Account account = new Account(mAccountName, MainApp.getAccountType()); + if (!AccountUtils.exists(account, MainApp.getAppContext())) { + // Cancel tranfers + if (mUploaderBinder != null) { + mUploaderBinder.cancel(account); + } + if (mDownloaderBinder != null) { + mDownloaderBinder.cancel(account); + } + } + Account a = AccountUtils.getCurrentOwnCloudAccount(this); String accountName = ""; if (a == null) { @@ -444,6 +539,16 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa @Override protected void onDestroy() { mDbHandler.close(); + + if (mDownloadServiceConnection != null) { + unbindService(mDownloadServiceConnection); + mDownloadServiceConnection = null; + } + if (mUploadServiceConnection != null) { + unbindService(mUploadServiceConnection); + mUploadServiceConnection = null; + } + super.onDestroy(); } @@ -587,4 +692,65 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa editor.putString("instant_video_upload_path", mUploadVideoPath); editor.commit(); } + + // Methods for ComponetsGetter + @Override + public FileDownloader.FileDownloaderBinder getFileDownloaderBinder() { + return mDownloaderBinder; + } + + + @Override + public FileUploader.FileUploaderBinder getFileUploaderBinder() { + return mUploaderBinder; + } + + @Override + public OperationsService.OperationsServiceBinder getOperationsServiceBinder() { + return null; + } + + @Override + public FileDataStorageManager getStorageManager() { + return null; + } + + @Override + public FileOperationsHelper getFileOperationsHelper() { + return null; + } + + protected ServiceConnection newTransferenceServiceConnection() { + return new PreferencesServiceConnection(); + } + + /** Defines callbacks for service binding, passed to bindService() */ + private class PreferencesServiceConnection implements ServiceConnection { + + @Override + public void onServiceConnected(ComponentName component, IBinder service) { + + if (component.equals(new ComponentName(Preferences.this, FileDownloader.class))) { + mDownloaderBinder = (FileDownloader.FileDownloaderBinder) service; + + } else if (component.equals(new ComponentName(Preferences.this, FileUploader.class))) { + Log_OC.d(TAG, "Upload service connected"); + mUploaderBinder = (FileUploader.FileUploaderBinder) service; + } else { + return; + } + + } + + @Override + public void onServiceDisconnected(ComponentName component) { + if (component.equals(new ComponentName(Preferences.this, FileDownloader.class))) { + Log_OC.d(TAG, "Download service suddenly disconnected"); + mDownloaderBinder = null; + } else if (component.equals(new ComponentName(Preferences.this, FileUploader.class))) { + Log_OC.d(TAG, "Upload service suddenly disconnected"); + mUploaderBinder = null; + } + } + }; } diff --git a/src/com/owncloud/android/ui/activity/UploadFilesActivity.java b/src/com/owncloud/android/ui/activity/UploadFilesActivity.java index 09185726..7563ea66 100644 --- a/src/com/owncloud/android/ui/activity/UploadFilesActivity.java +++ b/src/com/owncloud/android/ui/activity/UploadFilesActivity.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -48,9 +51,6 @@ import com.owncloud.android.utils.FileStorageUtils; /** * Displays local files and let the user choose what of them wants to upload * to the current ownCloud account - * - * @author David A. Velasco - * */ public class UploadFilesActivity extends FileActivity implements @@ -289,8 +289,6 @@ public class UploadFilesActivity extends FileActivity implements * to upload into the ownCloud local folder. * * Maybe an AsyncTask is not strictly necessary, but who really knows. - * - * @author David A. Velasco */ private class CheckAvailableSpaceTask extends AsyncTask { diff --git a/src/com/owncloud/android/ui/activity/UploadPathActivity.java b/src/com/owncloud/android/ui/activity/UploadPathActivity.java index aa3b8aa2..d1509f95 100644 --- a/src/com/owncloud/android/ui/activity/UploadPathActivity.java +++ b/src/com/owncloud/android/ui/activity/UploadPathActivity.java @@ -1,5 +1,7 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * 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, diff --git a/src/com/owncloud/android/ui/activity/Uploader.java b/src/com/owncloud/android/ui/activity/Uploader.java index 6c8a1320..d8b1225c 100644 --- a/src/com/owncloud/android/ui/activity/Uploader.java +++ b/src/com/owncloud/android/ui/activity/Uploader.java @@ -1,6 +1,9 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -69,9 +72,6 @@ import com.owncloud.android.utils.DisplayUtils; /** * This can be used to upload things to an ownCloud instance. - * - * @author Bartek Przybylski - * */ public class Uploader extends SherlockListActivity implements OnItemClickListener, android.view.View.OnClickListener { private static final String TAG = "ownCloudUploader"; diff --git a/src/com/owncloud/android/ui/adapter/CertificateCombinedExceptionViewAdapter.java b/src/com/owncloud/android/ui/adapter/CertificateCombinedExceptionViewAdapter.java index b1c32634..2528cb2d 100644 --- a/src/com/owncloud/android/ui/adapter/CertificateCombinedExceptionViewAdapter.java +++ b/src/com/owncloud/android/ui/adapter/CertificateCombinedExceptionViewAdapter.java @@ -1,5 +1,9 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author masensio + * @author David A. Velasco + * 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, @@ -25,9 +29,6 @@ import android.widget.TextView; /** * TODO - * - * @author masensio - * @author David A. Velasco * */ public class CertificateCombinedExceptionViewAdapter implements SslUntrustedCertDialog.ErrorViewAdapter { diff --git a/src/com/owncloud/android/ui/adapter/DiskLruImageCache.java b/src/com/owncloud/android/ui/adapter/DiskLruImageCache.java index 93efdf1c..0f2536f5 100644 --- a/src/com/owncloud/android/ui/adapter/DiskLruImageCache.java +++ b/src/com/owncloud/android/ui/adapter/DiskLruImageCache.java @@ -1,5 +1,7 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * 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, diff --git a/src/com/owncloud/android/ui/adapter/FileListListAdapter.java b/src/com/owncloud/android/ui/adapter/FileListListAdapter.java index 15f6a8bb..fbdf69d8 100644 --- a/src/com/owncloud/android/ui/adapter/FileListListAdapter.java +++ b/src/com/owncloud/android/ui/adapter/FileListListAdapter.java @@ -1,6 +1,11 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski + * @author Tobias Kaminsky + * @author David A. Velasco * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2014 ownCloud Inc. + * 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, @@ -19,11 +24,8 @@ 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; @@ -33,11 +35,12 @@ import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AbsListView; import android.widget.BaseAdapter; +import android.widget.GridView; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListAdapter; -import android.widget.ListView; import android.widget.TextView; import com.owncloud.android.R; @@ -45,9 +48,9 @@ import com.owncloud.android.authentication.AccountUtils; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.ThumbnailsCacheManager; -import com.owncloud.android.datamodel.ThumbnailsCacheManager.AsyncDrawable; 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; @@ -56,52 +59,47 @@ import com.owncloud.android.utils.FileStorageUtils; /** * This Adapter populates a ListView with all files and folders in an ownCloud * instance. - * - * @author Bartek Przybylski - * @author Tobias Kaminsky - * @author David A. Velasco */ public class FileListListAdapter extends BaseAdapter implements ListAdapter { private final static String PERMISSION_SHARED_WITH_ME = "S"; - + private Context mContext; private OCFile mFile = null; private Vector mFiles = null; + private Vector mFilesOrig = new Vector(); private boolean mJustFolders; private FileDataStorageManager mStorageManager; private Account mAccount; private ComponentsGetter mTransferServiceGetter; - 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 boolean mGridMode; + + private enum ViewType {LIST_ITEM, GRID_IMAGE, GRID_ITEM }; + private SharedPreferences mAppPreferences; public FileListListAdapter( boolean justFolders, - Context context, + Context context, ComponentsGetter transferServiceGetter ) { - + mJustFolders = justFolders; mContext = context; mAccount = AccountUtils.getCurrentOwnCloudAccount(mContext); - mTransferServiceGetter = transferServiceGetter; - + mAppPreferences = PreferenceManager .getDefaultSharedPreferences(mContext); // Read sorting order, default to sort by name ascending - mSortOrder = mAppPreferences - .getInt("sortOrder", 0); - mSortAscending = mAppPreferences.getBoolean("sortAscending", true); + FileStorageUtils.mSortOrder = mAppPreferences.getInt("sortOrder", 0); + FileStorageUtils.mSortAscending = mAppPreferences.getBoolean("sortAscending", true); // initialise thumbnails cache on background thread new ThumbnailsCacheManager.InitDiskCacheTask().execute(); + mGridMode = false; } @Override @@ -140,146 +138,190 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter { @Override public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; - if (view == null) { - LayoutInflater inflator = (LayoutInflater) mContext - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - view = inflator.inflate(R.layout.list_item, null); - } - + OCFile file = null; + LayoutInflater inflator = (LayoutInflater) mContext + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + if (mFiles != null && mFiles.size() > position) { - OCFile file = mFiles.get(position); - TextView fileName = (TextView) view.findViewById(R.id.Filename); + file = mFiles.get(position); + } + + // Find out which layout should be displayed + ViewType viewType; + if (!mGridMode){ + viewType = ViewType.LIST_ITEM; + } else if (file.isImage()){ + viewType = ViewType.GRID_IMAGE; + } else { + viewType = ViewType.GRID_ITEM; + } + + // Create View + switch (viewType){ + case GRID_IMAGE: + view = inflator.inflate(R.layout.grid_image, null); + break; + case GRID_ITEM: + view = inflator.inflate(R.layout.grid_item, null); + break; + case LIST_ITEM: + view = inflator.inflate(R.layout.list_item, null); + break; + } + + view.invalidate(); + + if (file != null){ + + ImageView fileIcon = (ImageView) view.findViewById(R.id.thumbnail); + + fileIcon.setTag(file.getFileId()); + TextView fileName; String name = file.getFileName(); LinearLayout linearLayout = (LinearLayout) view.findViewById(R.id.ListItemLayout); linearLayout.setContentDescription("LinearLayout-" + name); - fileName.setText(name); - ImageView fileIcon = (ImageView) view.findViewById(R.id.imageView1); - fileIcon.setTag(file.getFileId()); - ImageView sharedIconV = (ImageView) view.findViewById(R.id.sharedIcon); - ImageView sharedWithMeIconV = (ImageView) view.findViewById(R.id.sharedWithMeIcon); - sharedWithMeIconV.setVisibility(View.GONE); - - ImageView localStateView = (ImageView) view.findViewById(R.id.imageView2); - localStateView.bringToFront(); - FileDownloaderBinder downloaderBinder = - mTransferServiceGetter.getFileDownloaderBinder(); - FileUploaderBinder uploaderBinder = mTransferServiceGetter.getFileUploaderBinder(); - if (downloaderBinder != null && downloaderBinder.isDownloading(mAccount, file)) { - localStateView.setImageResource(R.drawable.downloading_file_indicator); - localStateView.setVisibility(View.VISIBLE); - } else if (uploaderBinder != null && uploaderBinder.isUploading(mAccount, file)) { - localStateView.setImageResource(R.drawable.uploading_file_indicator); - localStateView.setVisibility(View.VISIBLE); - } else if (file.isDown()) { - localStateView.setImageResource(R.drawable.local_file_indicator); - localStateView.setVisibility(View.VISIBLE); - } else { - localStateView.setVisibility(View.INVISIBLE); + switch (viewType){ + case LIST_ITEM: + TextView fileSizeV = (TextView) view.findViewById(R.id.file_size); + TextView lastModV = (TextView) view.findViewById(R.id.last_mod); + ImageView checkBoxV = (ImageView) view.findViewById(R.id.custom_checkbox); + + lastModV.setVisibility(View.VISIBLE); + lastModV.setText(showRelativeTimestamp(file)); + + checkBoxV.setVisibility(View.GONE); + + fileSizeV.setVisibility(View.VISIBLE); + fileSizeV.setText(DisplayUtils.bytesToHumanReadable(file.getFileLength())); + + if (!file.isFolder()) { + AbsListView parentList = (AbsListView)parent; + if (parentList.getChoiceMode() == AbsListView.CHOICE_MODE_NONE) { + checkBoxV.setVisibility(View.GONE); + } else { + if (parentList.isItemChecked(position)) { + checkBoxV.setImageResource(android.R.drawable.checkbox_on_background); + } else { + checkBoxV.setImageResource(android.R.drawable.checkbox_off_background); + } + checkBoxV.setVisibility(View.VISIBLE); + } + + } else { //Folder + fileSizeV.setVisibility(View.INVISIBLE); + } + + case GRID_ITEM: + // filename + fileName = (TextView) view.findViewById(R.id.Filename); + name = file.getFileName(); + fileName.setText(name); + + case GRID_IMAGE: + // sharedIcon + ImageView sharedIconV = (ImageView) view.findViewById(R.id.sharedIcon); + if (file.isShareByLink()) { + sharedIconV.setVisibility(View.VISIBLE); + sharedIconV.bringToFront(); + } else { + sharedIconV.setVisibility(View.GONE); + } + + // local state + ImageView localStateView = (ImageView) view.findViewById(R.id.localFileIndicator); + localStateView.bringToFront(); + FileDownloaderBinder downloaderBinder = mTransferServiceGetter.getFileDownloaderBinder(); + FileUploaderBinder uploaderBinder = mTransferServiceGetter.getFileUploaderBinder(); + 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)) { + localStateView.setImageResource(R.drawable.uploading_file_indicator); + localStateView.setVisibility(View.VISIBLE); + } else if (file.isDown()) { + localStateView.setImageResource(R.drawable.local_file_indicator); + localStateView.setVisibility(View.VISIBLE); + } else { + localStateView.setVisibility(View.INVISIBLE); + } + + // share with me icon + if (!file.isFolder()) { + ImageView sharedWithMeIconV = (ImageView) view.findViewById(R.id.sharedWithMeIcon); + sharedWithMeIconV.bringToFront(); + if (checkIfFileIsSharedWithMe(file)) { + sharedWithMeIconV.setVisibility(View.VISIBLE); + } else { + sharedWithMeIconV.setVisibility(View.GONE); + } + } + + break; } - TextView fileSizeV = (TextView) view.findViewById(R.id.file_size); - TextView lastModV = (TextView) view.findViewById(R.id.last_mod); - ImageView checkBoxV = (ImageView) view.findViewById(R.id.custom_checkbox); + // For all Views + // this if-else is needed even though favorite icon is visible by default + // because android reuses views in listview + if (!file.keepInSync()) { + view.findViewById(R.id.favoriteIcon).setVisibility(View.GONE); + } else { + view.findViewById(R.id.favoriteIcon).setVisibility(View.VISIBLE); + } + + // No Folder if (!file.isFolder()) { - fileSizeV.setVisibility(View.VISIBLE); - fileSizeV.setText(DisplayUtils.bytesToHumanReadable(file.getFileLength())); - lastModV.setVisibility(View.VISIBLE); - lastModV.setText(showRelativeTimestamp(file)); - // this if-else is needed even thoe fav icon is visible by default - // because android reuses views in listview - if (!file.keepInSync()) { - view.findViewById(R.id.imageView3).setVisibility(View.GONE); - } else { - view.findViewById(R.id.imageView3).setVisibility(View.VISIBLE); - } - - ListView parentList = (ListView)parent; - if (parentList.getChoiceMode() == ListView.CHOICE_MODE_NONE) { - checkBoxV.setVisibility(View.GONE); - } else { - if (parentList.isItemChecked(position)) { - checkBoxV.setImageResource(android.R.drawable.checkbox_on_background); - } else { - checkBoxV.setImageResource(android.R.drawable.checkbox_off_background); - } - checkBoxV.setVisibility(View.VISIBLE); - } - - // get Thumbnail if file is image if (file.isImage() && file.getRemoteId() != null){ - // Thumbnail in Cache? + // Thumbnail in Cache? Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( String.valueOf(file.getRemoteId()) - ); + ); if (thumbnail != null && !file.needsUpdateThumbnail()){ fileIcon.setImageBitmap(thumbnail); } else { // generate new Thumbnail if (ThumbnailsCacheManager.cancelPotentialWork(file, fileIcon)) { - final ThumbnailsCacheManager.ThumbnailGenerationTask task = + final ThumbnailsCacheManager.ThumbnailGenerationTask task = new ThumbnailsCacheManager.ThumbnailGenerationTask( fileIcon, mStorageManager, mAccount - ); + ); if (thumbnail == null) { thumbnail = ThumbnailsCacheManager.mDefaultImg; } - final AsyncDrawable asyncDrawable = new AsyncDrawable( + final ThumbnailsCacheManager.AsyncDrawable asyncDrawable = + new ThumbnailsCacheManager.AsyncDrawable( mContext.getResources(), thumbnail, task - ); + ); fileIcon.setImageDrawable(asyncDrawable); task.execute(file); } } } else { - fileIcon.setImageResource( - DisplayUtils.getResourceId(file.getMimetype(), file.getFileName()) - ); - } - - if (checkIfFileIsSharedWithMe(file)) { - sharedWithMeIconV.setVisibility(View.VISIBLE); + fileIcon.setImageResource(DisplayUtils.getFileTypeIconId(file.getMimetype(), file.getFileName())); } - } - else { - // TODO Re-enable when server supports folder-size calculation -// if (FileStorageUtils.getDefaultSavePathFor(mAccount.name, file) != null){ -// fileSizeV.setVisibility(View.VISIBLE); -// fileSizeV.setText(getFolderSizeHuman(FileStorageUtils.getDefaultSavePathFor(mAccount.name, file))); -// } else { - fileSizeV.setVisibility(View.INVISIBLE); -// } - - lastModV.setVisibility(View.VISIBLE); - lastModV.setText(showRelativeTimestamp(file)); - checkBoxV.setVisibility(View.GONE); - view.findViewById(R.id.imageView3).setVisibility(View.GONE); + } else { + // Folder if (checkIfFileIsSharedWithMe(file)) { fileIcon.setImageResource(R.drawable.shared_with_me_folder); - sharedWithMeIconV.setVisibility(View.VISIBLE); + } else if (file.isShareByLink()) { + // If folder is sharedByLink, icon folder must be changed to + // folder-public one + fileIcon.setImageResource(R.drawable.folder_public); } else { fileIcon.setImageResource( - DisplayUtils.getResourceId(file.getMimetype(), file.getFileName()) + DisplayUtils.getFileTypeIconId(file.getMimetype(), file.getFileName()) ); } - - // If folder is sharedByLink, icon folder must be changed to - // folder-public one - if (file.isShareByLink()) { - fileIcon.setImageResource(R.drawable.folder_public); - } - } - - if (file.isShareByLink()) { - sharedIconV.setVisibility(View.VISIBLE); - } else { - sharedIconV.setVisibility(View.GONE); } } @@ -298,7 +340,7 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter { File dir = new File(path); if (dir.exists()) { - long bytes = getFolderSize(dir); + long bytes = FileStorageUtils.getFolderSize(dir); return DisplayUtils.bytesToHumanReadable(bytes); } @@ -356,6 +398,9 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter { } if (mStorageManager != null) { mFiles = mStorageManager.getFolderContent(mFile); + mFilesOrig.clear(); + mFilesOrig.addAll(mFiles); + if (mJustFolders) { mFiles = getFolders(mFiles); } @@ -363,29 +408,11 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter { mFiles = null; } - sortDirectory(); - } - - /** - * Sorts all filenames, regarding last user decision - */ - private void sortDirectory(){ - switch (mSortOrder){ - case 0: - sortByName(mSortAscending); - break; - case 1: - sortByDate(mSortAscending); - break; - case 2: - sortBySize(mSortAscending); - break; - } - + mFiles = FileStorageUtils.sortFolder(mFiles); notifyDataSetChanged(); } - + /** * Filter for getting only the folders * @param files @@ -418,110 +445,27 @@ 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() { - 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() { - 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() { - 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); editor.putBoolean("sortAscending", ascending); editor.commit(); - mSortOrder = order; - mSortAscending = ascending; + FileStorageUtils.mSortOrder = order; + FileStorageUtils.mSortAscending = ascending; - sortDirectory(); - } + + mFiles = FileStorageUtils.sortFolder(mFiles); + notifyDataSetChanged(); + + } private CharSequence showRelativeTimestamp(OCFile file){ return DisplayUtils.getRelativeDateTimeString(mContext, file.getModificationTimestamp(), DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0); } + + public void setGridMode(boolean gridMode) { + mGridMode = gridMode; + } } diff --git a/src/com/owncloud/android/ui/adapter/LocalFileListAdapter.java b/src/com/owncloud/android/ui/adapter/LocalFileListAdapter.java index 6190ebee..823abc5c 100644 --- a/src/com/owncloud/android/ui/adapter/LocalFileListAdapter.java +++ b/src/com/owncloud/android/ui/adapter/LocalFileListAdapter.java @@ -1,6 +1,9 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author David A. Velasco * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2014 ownCloud Inc. + * 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, @@ -22,6 +25,7 @@ import java.util.Arrays; import java.util.Comparator; import android.content.Context; +import android.graphics.Bitmap; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -32,21 +36,22 @@ import android.widget.ListView; import android.widget.TextView; import com.owncloud.android.R; +import com.owncloud.android.datamodel.ThumbnailsCacheManager; +import com.owncloud.android.utils.BitmapUtils; import com.owncloud.android.utils.DisplayUtils; +import third_parties.in.srain.cube.GridViewWithHeaderAndFooter; + /** * This Adapter populates a ListView with all files and directories contained * in a local directory - * - * @author David A. Velasco - * */ public class LocalFileListAdapter extends BaseAdapter implements ListAdapter { private Context mContext; private File mDirectory; private File[] mFiles = null; - + public LocalFileListAdapter(File directory, Context context) { mContext = context; swapDirectory(directory); @@ -99,12 +104,13 @@ public class LocalFileListAdapter extends BaseAdapter implements ListAdapter { String name = file.getName(); fileName.setText(name); - ImageView fileIcon = (ImageView) view.findViewById(R.id.imageView1); + ImageView fileIcon = (ImageView) view.findViewById(R.id.thumbnail); if (!file.isDirectory()) { fileIcon.setImageResource(R.drawable.file); } else { fileIcon.setImageResource(R.drawable.ic_menu_archive); } + fileIcon.setTag(file.hashCode()); TextView fileSizeV = (TextView) view.findViewById(R.id.file_size); TextView lastModV = (TextView) view.findViewById(R.id.last_mod); @@ -112,9 +118,10 @@ public class LocalFileListAdapter extends BaseAdapter implements ListAdapter { if (!file.isDirectory()) { fileSizeV.setVisibility(View.VISIBLE); fileSizeV.setText(DisplayUtils.bytesToHumanReadable(file.length())); + lastModV.setVisibility(View.VISIBLE); lastModV.setText(DisplayUtils.unixTimeToHumanReadable(file.lastModified())); - ListView parentList = (ListView)parent; + ListView parentList = (ListView) parent; if (parentList.getChoiceMode() == ListView.CHOICE_MODE_NONE) { checkBoxV.setVisibility(View.GONE); } else { @@ -125,15 +132,47 @@ public class LocalFileListAdapter extends BaseAdapter implements ListAdapter { } checkBoxV.setVisibility(View.VISIBLE); } + + // get Thumbnail if file is image + if (BitmapUtils.isImage(file)){ + // Thumbnail in Cache? + Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( + String.valueOf(file.hashCode()) + ); + if (thumbnail != null){ + fileIcon.setImageBitmap(thumbnail); + } else { + + // generate new Thumbnail + if (ThumbnailsCacheManager.cancelPotentialWork(file, fileIcon)) { + final ThumbnailsCacheManager.ThumbnailGenerationTask task = + new ThumbnailsCacheManager.ThumbnailGenerationTask(fileIcon); + if (thumbnail == null) { + thumbnail = ThumbnailsCacheManager.mDefaultImg; + } + final ThumbnailsCacheManager.AsyncDrawable asyncDrawable = + new ThumbnailsCacheManager.AsyncDrawable( + mContext.getResources(), + thumbnail, + task + ); + fileIcon.setImageDrawable(asyncDrawable); + task.execute(file); + } + } + } else { + fileIcon.setImageResource(DisplayUtils.getFileTypeIconId(null, file.getName())); + } } else { fileSizeV.setVisibility(View.GONE); lastModV.setVisibility(View.GONE); checkBoxV.setVisibility(View.GONE); } - - view.findViewById(R.id.imageView2).setVisibility(View.INVISIBLE); // not GONE; the alignment changes; ugly way to keep it - view.findViewById(R.id.imageView3).setVisibility(View.GONE); + + // not GONE; the alignment changes; ugly way to keep it + view.findViewById(R.id.localFileIndicator).setVisibility(View.INVISIBLE); + view.findViewById(R.id.favoriteIcon).setVisibility(View.GONE); view.findViewById(R.id.sharedIcon).setVisibility(View.GONE); view.findViewById(R.id.sharedWithMeIcon).setVisibility(View.GONE); diff --git a/src/com/owncloud/android/ui/adapter/LogListAdapter.java b/src/com/owncloud/android/ui/adapter/LogListAdapter.java index ae4335ef..b5664d55 100644 --- a/src/com/owncloud/android/ui/adapter/LogListAdapter.java +++ b/src/com/owncloud/android/ui/adapter/LogListAdapter.java @@ -1,3 +1,22 @@ +/** + * 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 . + * + */ + package com.owncloud.android.ui.adapter; import java.io.File; diff --git a/src/com/owncloud/android/ui/adapter/SslCertificateViewAdapter.java b/src/com/owncloud/android/ui/adapter/SslCertificateViewAdapter.java index a944eadb..71656149 100644 --- a/src/com/owncloud/android/ui/adapter/SslCertificateViewAdapter.java +++ b/src/com/owncloud/android/ui/adapter/SslCertificateViewAdapter.java @@ -1,5 +1,9 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author masensio + * @author David A. Velasco + * 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, @@ -27,9 +31,6 @@ import android.widget.TextView; /** * TODO - * - * @author masensio - * @author David A. Velasco */ public class SslCertificateViewAdapter implements SslUntrustedCertDialog.CertificateViewAdapter { diff --git a/src/com/owncloud/android/ui/adapter/SslErrorViewAdapter.java b/src/com/owncloud/android/ui/adapter/SslErrorViewAdapter.java index 7d2e291b..7ac71446 100644 --- a/src/com/owncloud/android/ui/adapter/SslErrorViewAdapter.java +++ b/src/com/owncloud/android/ui/adapter/SslErrorViewAdapter.java @@ -1,5 +1,9 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author masensio + * @author David A. Velasco + * 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, @@ -24,10 +28,6 @@ import android.widget.TextView; /** * Dialog to show an Untrusted Certificate - * - * @author masensio - * @author David A. Velasco - * */ public class SslErrorViewAdapter implements SslUntrustedCertDialog.ErrorViewAdapter { diff --git a/src/com/owncloud/android/ui/adapter/X509CertificateViewAdapter.java b/src/com/owncloud/android/ui/adapter/X509CertificateViewAdapter.java index a290dca2..1c8c8c28 100644 --- a/src/com/owncloud/android/ui/adapter/X509CertificateViewAdapter.java +++ b/src/com/owncloud/android/ui/adapter/X509CertificateViewAdapter.java @@ -1,5 +1,9 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author masensio + * @author David A. Velasco + * 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, @@ -31,9 +35,6 @@ import android.view.View; import android.widget.TextView; /** - * - * @author masensio - * @author David A. Velasco * */ public class X509CertificateViewAdapter implements SslUntrustedCertDialog.CertificateViewAdapter { diff --git a/src/com/owncloud/android/ui/dialog/ChangelogDialog.java b/src/com/owncloud/android/ui/dialog/ChangelogDialog.java index eef9d09b..24d7bc25 100644 --- a/src/com/owncloud/android/ui/dialog/ChangelogDialog.java +++ b/src/com/owncloud/android/ui/dialog/ChangelogDialog.java @@ -1,5 +1,7 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * 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, diff --git a/src/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.java b/src/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.java index a9307b9c..29ed3908 100644 --- a/src/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.java +++ b/src/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, diff --git a/src/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java b/src/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java index 91cfbfde..6cf229df 100644 --- a/src/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java +++ b/src/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java @@ -1,6 +1,9 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -33,9 +36,6 @@ import com.owncloud.android.utils.DisplayUtils; /** * Dialog which will be displayed to user upon keep-in-sync file conflict. - * - * @author Bartek Przybylski - * */ public class ConflictsResolveDialog extends SherlockDialogFragment { diff --git a/src/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.java b/src/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.java index 29b3be28..170fe08a 100644 --- a/src/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.java +++ b/src/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -37,9 +40,7 @@ import android.widget.Toast; /** * Dialog to input the name for a new folder to create. * - * Triggers the folder creation when name is confirmed. - * - * @author David A. Velasco + * Triggers the folder creation when name is confirmed. */ public class CreateFolderDialogFragment extends SherlockDialogFragment implements DialogInterface.OnClickListener { diff --git a/src/com/owncloud/android/ui/dialog/CredentialsDialogFragment.java b/src/com/owncloud/android/ui/dialog/CredentialsDialogFragment.java index 080316b8..1b99c7b0 100644 --- a/src/com/owncloud/android/ui/dialog/CredentialsDialogFragment.java +++ b/src/com/owncloud/android/ui/dialog/CredentialsDialogFragment.java @@ -1,5 +1,7 @@ -/* ownCloud Android client application - * Copyright (C) 2014 ownCloud Inc. +/** + * 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, diff --git a/src/com/owncloud/android/ui/dialog/IndeterminateProgressDialog.java b/src/com/owncloud/android/ui/dialog/IndeterminateProgressDialog.java index dbd3d991..32fa4c65 100644 --- a/src/com/owncloud/android/ui/dialog/IndeterminateProgressDialog.java +++ b/src/com/owncloud/android/ui/dialog/IndeterminateProgressDialog.java @@ -1,5 +1,7 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * 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, diff --git a/src/com/owncloud/android/ui/dialog/LoadingDialog.java b/src/com/owncloud/android/ui/dialog/LoadingDialog.java index d0dcfb7b..e8e68e71 100644 --- a/src/com/owncloud/android/ui/dialog/LoadingDialog.java +++ b/src/com/owncloud/android/ui/dialog/LoadingDialog.java @@ -1,5 +1,7 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * 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, diff --git a/src/com/owncloud/android/ui/dialog/RemoveFileDialogFragment.java b/src/com/owncloud/android/ui/dialog/RemoveFileDialogFragment.java index 24534047..c3b7ed1e 100644 --- a/src/com/owncloud/android/ui/dialog/RemoveFileDialogFragment.java +++ b/src/com/owncloud/android/ui/dialog/RemoveFileDialogFragment.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -20,9 +23,7 @@ package com.owncloud.android.ui.dialog; /** * Dialog requiring confirmation before removing a given OCFile. * - * Triggers the removal according to the user response. - * - * @author David A. Velasco + * Triggers the removal according to the user response. */ import java.util.Vector; diff --git a/src/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java b/src/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java index d285f1e4..0e7850b5 100644 --- a/src/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java +++ b/src/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java @@ -1,4 +1,7 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author David A. Velasco * Copyright (C) 2014 ownCloud Inc. * * This program is free software: you can redistribute it and/or modify @@ -43,9 +46,7 @@ import com.owncloud.android.ui.activity.ComponentsGetter; /** * Dialog to input a new name for a file or folder to rename. * - * Triggers the rename operation when name is confirmed. - * - * @author David A. Velasco + * Triggers the rename operation when name is confirmed. */ public class RenameFileDialogFragment extends SherlockDialogFragment implements DialogInterface.OnClickListener { diff --git a/src/com/owncloud/android/ui/dialog/SamlWebViewDialog.java b/src/com/owncloud/android/ui/dialog/SamlWebViewDialog.java index 76243edf..31d1d2dc 100644 --- a/src/com/owncloud/android/ui/dialog/SamlWebViewDialog.java +++ b/src/com/owncloud/android/ui/dialog/SamlWebViewDialog.java @@ -1,5 +1,9 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author Maria Asensio + * @author David A. Velasco + * 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, @@ -44,9 +48,6 @@ import com.owncloud.android.lib.common.utils.Log_OC; /** * Dialog to show the WebView for SAML Authentication - * - * @author Maria Asensio - * @author David A. Velasco */ public class SamlWebViewDialog extends SherlockDialogFragment { @@ -76,7 +77,6 @@ public class SamlWebViewDialog extends SherlockDialogFragment { * @return New dialog instance, ready to show. */ public static SamlWebViewDialog newInstance(String url, String targetUrl) { - Log_OC.d(TAG, "New instance"); SamlWebViewDialog fragment = new SamlWebViewDialog(); Bundle args = new Bundle(); args.putString(ARG_INITIAL_URL, url); @@ -88,13 +88,12 @@ public class SamlWebViewDialog extends SherlockDialogFragment { public SamlWebViewDialog() { super(); - Log_OC.d(TAG, "constructor"); } @Override public void onAttach(Activity activity) { - Log_OC.d(TAG, "onAttach"); + Log_OC.v(TAG, "onAttach"); super.onAttach(activity); try { mSsoWebViewClientListener = (SsoWebViewClientListener) activity; @@ -110,7 +109,7 @@ public class SamlWebViewDialog extends SherlockDialogFragment { @SuppressLint("SetJavaScriptEnabled") @Override public void onCreate(Bundle savedInstanceState) { - Log_OC.d(TAG, "onCreate, savedInstanceState is " + savedInstanceState); + Log_OC.v(TAG, "onCreate, savedInstanceState is " + savedInstanceState); super.onCreate(savedInstanceState); setRetainInstance(true); @@ -132,7 +131,7 @@ public class SamlWebViewDialog extends SherlockDialogFragment { @SuppressLint("SetJavaScriptEnabled") @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - Log_OC.d(TAG, "onCreateView, savedInsanceState is " + savedInstanceState); + Log_OC.v(TAG, "onCreateView, savedInsanceState is " + savedInstanceState); // Inflate layout of the dialog RelativeLayout ssoRootView = (RelativeLayout) inflater.inflate(R.layout.sso_dialog, container, false); // null parent view because it will go in the dialog layout @@ -144,11 +143,6 @@ public class SamlWebViewDialog extends SherlockDialogFragment { mSsoWebView.setFocusableInTouchMode(true); mSsoWebView.setClickable(true); - CookieManager cookieManager = CookieManager.getInstance(); - cookieManager.setAcceptCookie(true); - cookieManager.removeAllCookie(); - mSsoWebView.loadUrl(mInitialUrl); - WebSettings webSettings = mSsoWebView.getSettings(); webSettings.setJavaScriptEnabled(true); webSettings.setBuiltInZoomControls(false); @@ -156,6 +150,12 @@ public class SamlWebViewDialog extends SherlockDialogFragment { webSettings.setSavePassword(false); webSettings.setUserAgentString(OwnCloudClient.USER_AGENT); webSettings.setSaveFormData(false); + + CookieManager cookieManager = CookieManager.getInstance(); + cookieManager.setAcceptCookie(true); + cookieManager.removeAllCookie(); + + mSsoWebView.loadUrl(mInitialUrl); } mWebViewClient.setTargetUrl(mTargetUrl); @@ -174,7 +174,7 @@ public class SamlWebViewDialog extends SherlockDialogFragment { @Override public void onSaveInstanceState(Bundle outState) { - Log_OC.d(TAG, "onSaveInstanceState being CALLED"); + Log_OC.v(TAG, "onSaveInstanceState being CALLED"); super.onSaveInstanceState(outState); // save URLs @@ -184,7 +184,7 @@ public class SamlWebViewDialog extends SherlockDialogFragment { @Override public void onDestroyView() { - Log_OC.d(TAG, "onDestroyView"); + Log_OC.v(TAG, "onDestroyView"); if ((ViewGroup)mSsoWebView.getParent() != null) { ((ViewGroup)mSsoWebView.getParent()).removeView(mSsoWebView); @@ -196,8 +196,6 @@ public class SamlWebViewDialog extends SherlockDialogFragment { Dialog dialog = getDialog(); if ((dialog != null)) { dialog.setOnDismissListener(null); - //dialog.dismiss(); - //dialog.setDismissMessage(null); } super.onDestroyView(); @@ -205,13 +203,13 @@ public class SamlWebViewDialog extends SherlockDialogFragment { @Override public void onDestroy() { - Log_OC.d(TAG, "onDestroy"); + Log_OC.v(TAG, "onDestroy"); super.onDestroy(); } @Override public void onDetach() { - Log_OC.d(TAG, "onDetach"); + Log_OC.v(TAG, "onDetach"); mSsoWebViewClientListener = null; mWebViewClient = null; super.onDetach(); @@ -231,39 +229,39 @@ public class SamlWebViewDialog extends SherlockDialogFragment { @Override public void onStart() { - Log_OC.d(TAG, "onStart"); + Log_OC.v(TAG, "onStart"); super.onStart(); } @Override public void onStop() { - Log_OC.d(TAG, "onStop"); + Log_OC.v(TAG, "onStop"); super.onStop(); } @Override public void onResume() { - Log_OC.d(TAG, "onResume"); + Log_OC.v(TAG, "onResume"); super.onResume(); mSsoWebView.onResume(); } @Override public void onPause() { - Log_OC.d(TAG, "onPause"); + Log_OC.v(TAG, "onPause"); mSsoWebView.onPause(); super.onPause(); } @Override public int show (FragmentTransaction transaction, String tag) { - Log_OC.d(TAG, "show (transaction)"); + Log_OC.v(TAG, "show (transaction)"); return super.show(transaction, tag); } @Override public void show (FragmentManager manager, String tag) { - Log_OC.d(TAG, "show (manager)"); + Log_OC.v(TAG, "show (manager)"); super.show(manager, tag); } diff --git a/src/com/owncloud/android/ui/dialog/ShareLinkToDialog.java b/src/com/owncloud/android/ui/dialog/ShareLinkToDialog.java index 2876f7b1..e05f2165 100644 --- a/src/com/owncloud/android/ui/dialog/ShareLinkToDialog.java +++ b/src/com/owncloud/android/ui/dialog/ShareLinkToDialog.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -50,8 +53,6 @@ import com.owncloud.android.ui.activity.FileActivity; /** * Dialog showing a list activities able to resolve a given Intent, * filtering out the activities matching give package names. - * - * @author David A. Velasco */ public class ShareLinkToDialog extends SherlockDialogFragment { diff --git a/src/com/owncloud/android/ui/dialog/SslUntrustedCertDialog.java b/src/com/owncloud/android/ui/dialog/SslUntrustedCertDialog.java index 167177bd..66582608 100644 --- a/src/com/owncloud/android/ui/dialog/SslUntrustedCertDialog.java +++ b/src/com/owncloud/android/ui/dialog/SslUntrustedCertDialog.java @@ -1,5 +1,9 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author masensio + * @author David A. Velasco + * 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, @@ -47,10 +51,7 @@ import com.owncloud.android.ui.adapter.X509CertificateViewAdapter; * to decide trust on it or not. * * Abstract implementation of common functionality for different dialogs that - * get the information about the error and the certificate from different classes. - * - * @author masensio - * @author David A. Velasco + * get the information about the error and the certificate from different classes. */ public class SslUntrustedCertDialog extends SherlockDialogFragment { diff --git a/src/com/owncloud/android/ui/dialog/SslValidatorDialog.java b/src/com/owncloud/android/ui/dialog/SslValidatorDialog.java index db20a5cb..1e31c0ca 100644 --- a/src/com/owncloud/android/ui/dialog/SslValidatorDialog.java +++ b/src/com/owncloud/android/ui/dialog/SslValidatorDialog.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -46,8 +49,6 @@ import com.owncloud.android.lib.common.utils.Log_OC; /** * Dialog to request the user about a certificate that could not be validated with the certificates store in the system. - * - * @author David A. Velasco */ public class SslValidatorDialog extends Dialog { diff --git a/src/com/owncloud/android/ui/dialog/SsoWebView.java b/src/com/owncloud/android/ui/dialog/SsoWebView.java index 3a71139d..dae43f8e 100644 --- a/src/com/owncloud/android/ui/dialog/SsoWebView.java +++ b/src/com/owncloud/android/ui/dialog/SsoWebView.java @@ -1,5 +1,7 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * 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, diff --git a/src/com/owncloud/android/ui/fragment/AuthenticatorAccountDetailsFragment.java b/src/com/owncloud/android/ui/fragment/AuthenticatorAccountDetailsFragment.java index f0961037..1552da1e 100644 --- a/src/com/owncloud/android/ui/fragment/AuthenticatorAccountDetailsFragment.java +++ b/src/com/owncloud/android/ui/fragment/AuthenticatorAccountDetailsFragment.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, diff --git a/src/com/owncloud/android/ui/fragment/AuthenticatorGetStartedFragment.java b/src/com/owncloud/android/ui/fragment/AuthenticatorGetStartedFragment.java index 5abf55d8..5a8da70d 100644 --- a/src/com/owncloud/android/ui/fragment/AuthenticatorGetStartedFragment.java +++ b/src/com/owncloud/android/ui/fragment/AuthenticatorGetStartedFragment.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, diff --git a/src/com/owncloud/android/ui/fragment/ExtendedListFragment.java b/src/com/owncloud/android/ui/fragment/ExtendedListFragment.java index 1b7a1ddf..03289cf9 100644 --- a/src/com/owncloud/android/ui/fragment/ExtendedListFragment.java +++ b/src/com/owncloud/android/ui/fragment/ExtendedListFragment.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * 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, @@ -20,15 +22,17 @@ package com.owncloud.android.ui.fragment; import java.util.ArrayList; +import android.content.Context; import android.os.Bundle; import android.support.v4.widget.SwipeRefreshLayout; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; +import android.widget.GridView; import android.widget.ListAdapter; -import android.widget.ListView; import android.widget.TextView; import com.actionbarsherlock.app.SherlockFragment; @@ -36,6 +40,9 @@ import com.owncloud.android.R; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.ui.ExtendedListView; import com.owncloud.android.ui.activity.OnEnforceableRefreshListener; +import com.owncloud.android.ui.adapter.FileListListAdapter; + +import third_parties.in.srain.cube.GridViewWithHeaderAndFooter; /** * TODO extending SherlockListFragment instead of SherlockFragment @@ -52,9 +59,8 @@ implements OnItemClickListener, OnEnforceableRefreshListener { private static final String KEY_HEIGHT_CELL = "HEIGHT_CELL"; private static final String KEY_EMPTY_LIST_MESSAGE = "EMPTY_LIST_MESSAGE"; - protected ExtendedListView mList; - - private SwipeRefreshLayout mRefreshLayout; + private SwipeRefreshLayout mRefreshListLayout; + private SwipeRefreshLayout mRefreshGridLayout; private SwipeRefreshLayout mRefreshEmptyLayout; private TextView mEmptyListMessage; @@ -66,46 +72,98 @@ implements OnItemClickListener, OnEnforceableRefreshListener { private OnEnforceableRefreshListener mOnRefreshListener = null; - - public void setListAdapter(ListAdapter listAdapter) { - mList.setAdapter(listAdapter); - mList.invalidate(); + protected AbsListView mCurrentListView; + private ExtendedListView mListView; + private View mListFooterView; + private GridViewWithHeaderAndFooter mGridView; + private View mGridFooterView; + + private ListAdapter mAdapter; + + + protected void setListAdapter(ListAdapter listAdapter) { + mAdapter = listAdapter; + mCurrentListView.setAdapter(listAdapter); + mCurrentListView.invalidate(); } - public void setFooterView(View footer) { - mList.addFooterView(footer, null, false); - mList.invalidate(); + protected AbsListView getListView() { + return mCurrentListView; } - public ListView getListView() { - return mList; + + protected void switchToGridView() { + if ((mCurrentListView == mListView)) { + + mListView.setAdapter(null); + mRefreshListLayout.setVisibility(View.GONE); + + if (mAdapter instanceof FileListListAdapter) { + ((FileListListAdapter) mAdapter).setGridMode(true); + } + mGridView.setAdapter(mAdapter); + mRefreshGridLayout.setVisibility(View.VISIBLE); + + mCurrentListView = mGridView; + } } + + protected void switchToListView() { + if (mCurrentListView == mGridView) { + mGridView.setAdapter(null); + mRefreshGridLayout.setVisibility(View.GONE); + + if (mAdapter instanceof FileListListAdapter) { + ((FileListListAdapter) mAdapter).setGridMode(false); + } + mListView.setAdapter(mAdapter); + mRefreshListLayout.setVisibility(View.VISIBLE); + mCurrentListView = mListView; + } + } + + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - Log_OC.e(TAG, "onCreateView"); + Log_OC.d(TAG, "onCreateView"); View v = inflater.inflate(R.layout.list_fragment, null); - mEmptyListMessage = (TextView) v.findViewById(R.id.empty_list_view); - mList = (ExtendedListView) (v.findViewById(R.id.list_root)); - mList.setOnItemClickListener(this); - mList.setDivider(getResources().getDrawable(R.drawable.uploader_list_separator)); - mList.setDividerHeight(1); + mListView = (ExtendedListView)(v.findViewById(R.id.list_root)); + mListView.setOnItemClickListener(this); + mListFooterView = inflater.inflate(R.layout.list_footer, null, false); + + mGridView = (GridViewWithHeaderAndFooter) (v.findViewById(R.id.grid_root)); + mGridView.setNumColumns(GridView.AUTO_FIT); + mGridView.setOnItemClickListener(this); + mGridFooterView = inflater.inflate(R.layout.list_footer, null, false); if (savedInstanceState != null) { int referencePosition = savedInstanceState.getInt(KEY_SAVED_LIST_POSITION); - setReferencePosition(referencePosition); + if (mCurrentListView == mListView) { + Log_OC.v(TAG, "Setting and centering around list position " + referencePosition); + mListView.setAndCenterSelection(referencePosition); + } else { + Log_OC.v(TAG, "Setting grid position " + referencePosition); + mGridView.setSelection(referencePosition); + } } - // Pull down refresh - mRefreshLayout = (SwipeRefreshLayout) v.findViewById(R.id.swipe_refresh_files); - mRefreshEmptyLayout = (SwipeRefreshLayout) v.findViewById(R.id.swipe_refresh_files_emptyView); + // Pull-down to refresh layout + mRefreshListLayout = (SwipeRefreshLayout) v.findViewById(R.id.swipe_containing_list); + mRefreshGridLayout = (SwipeRefreshLayout) v.findViewById(R.id.swipe_containing_grid); + mRefreshEmptyLayout = (SwipeRefreshLayout) v.findViewById(R.id.swipe_containing_empty); + mEmptyListMessage = (TextView) v.findViewById(R.id.empty_list_view); - onCreateSwipeToRefresh(mRefreshLayout); + onCreateSwipeToRefresh(mRefreshListLayout); + onCreateSwipeToRefresh(mRefreshGridLayout); onCreateSwipeToRefresh(mRefreshEmptyLayout); - - mList.setEmptyView(mRefreshEmptyLayout); + + mListView.setEmptyView(mRefreshEmptyLayout); + mGridView.setEmptyView(mRefreshEmptyLayout); + + mCurrentListView = mListView; // list as default return v; } @@ -136,7 +194,7 @@ implements OnItemClickListener, OnEnforceableRefreshListener { @Override public void onSaveInstanceState(Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); - Log_OC.e(TAG, "onSaveInstanceState()"); + Log_OC.d(TAG, "onSaveInstanceState()"); savedInstanceState.putInt(KEY_SAVED_LIST_POSITION, getReferencePosition()); savedInstanceState.putIntegerArrayList(KEY_INDEXES, mIndexes); savedInstanceState.putIntegerArrayList(KEY_FIRST_POSITIONS, mFirstPositions); @@ -150,32 +208,20 @@ implements OnItemClickListener, OnEnforceableRefreshListener { * reposition the visible items in the list when the device is turned to * other position. * - * THe current policy is take as a reference the visible item in the center + * The current policy is take as a reference the visible item in the center * of the screen. * * @return The position in the list of the visible item in the center of the * screen. */ protected int getReferencePosition() { - if (mList != null) { - return (mList.getFirstVisiblePosition() + mList.getLastVisiblePosition()) / 2; + if (mCurrentListView != null) { + return (mCurrentListView.getFirstVisiblePosition() + mCurrentListView.getLastVisiblePosition()) / 2; } else { return 0; } } - /** - * Sets the visible part of the list from the reference position. - * - * @param position Reference position previously returned by - * {@link LocalFileListFragment#getReferencePosition()} - */ - protected void setReferencePosition(int position) { - if (mList != null) { - mList.setAndCenterSelection(position); - } - } - /* * Restore index and position @@ -185,28 +231,28 @@ implements OnItemClickListener, OnEnforceableRefreshListener { // needs to be checked; not every browse-up had a browse-down before int index = mIndexes.remove(mIndexes.size() - 1); - - int firstPosition = mFirstPositions.remove(mFirstPositions.size() -1); - + final int firstPosition = mFirstPositions.remove(mFirstPositions.size() -1); int top = mTops.remove(mTops.size() - 1); - - mList.setSelectionFromTop(firstPosition, top); - - // Move the scroll if the selection is not visible - int indexPosition = mHeightCell*index; - int height = mList.getHeight(); - - if (indexPosition > height) { - if (android.os.Build.VERSION.SDK_INT >= 11) - { - mList.smoothScrollToPosition(index); + + Log_OC.v(TAG, "Setting selection to position: " + firstPosition + "; top: " + top + "; index: " + index); + + if (mCurrentListView == mListView) { + if (mHeightCell*index <= mListView.getHeight()) { + mListView.setSelectionFromTop(firstPosition, top); + } else { + mListView.setSelectionFromTop(index, 0); } - else if (android.os.Build.VERSION.SDK_INT >= 8) - { - mList.setSelectionFromTop(index, 0); + + } else { + if (mHeightCell*index <= mGridView.getHeight()) { + mGridView.setSelection(firstPosition); + //mGridView.smoothScrollToPosition(firstPosition); + } else { + mGridView.setSelection(index); + //mGridView.smoothScrollToPosition(index); } - } + } } @@ -217,10 +263,10 @@ implements OnItemClickListener, OnEnforceableRefreshListener { mIndexes.add(index); - int firstPosition = mList.getFirstVisiblePosition(); + int firstPosition = mCurrentListView.getFirstVisiblePosition(); mFirstPositions.add(firstPosition); - View view = mList.getChildAt(0); + View view = mCurrentListView.getChildAt(0); int top = (view == null) ? 0 : view.getTop() ; mTops.add(top); @@ -237,10 +283,10 @@ implements OnItemClickListener, OnEnforceableRefreshListener { @Override public void onRefresh() { - // to be @overriden - mRefreshLayout.setRefreshing(false); + mRefreshListLayout.setRefreshing(false); + mRefreshGridLayout.setRefreshing(false); mRefreshEmptyLayout.setRefreshing(false); - + if (mOnRefreshListener != null) { mOnRefreshListener.onRefresh(); } @@ -251,32 +297,18 @@ implements OnItemClickListener, OnEnforceableRefreshListener { /** - * Enables swipe gesture - */ - public void enableSwipe() { - mRefreshLayout.setEnabled(true); - } - - /** - * Disables swipe gesture. It prevents manual gestures but keeps the option you show - * refreshing programmatically. - */ - public void disableSwipe() { - mRefreshLayout.setEnabled(false); - } - - /** - * It shows the SwipeRefreshLayout progress - */ - public void showSwipeProgress() { - mRefreshLayout.setRefreshing(true); - } - - /** - * It shows the SwipeRefreshLayout progress + * Disables swipe gesture. + * + * Sets the 'enabled' state of the refresh layouts contained in the fragment. + * + * When 'false' is set, prevents user gestures but keeps the option to refresh programatically, + * + * @param enabled Desired state for capturing swipe gesture. */ - public void hideSwipeProgress() { - mRefreshLayout.setRefreshing(false); + public void setSwipeEnabled(boolean enabled) { + mRefreshListLayout.setEnabled(enabled); + mRefreshGridLayout.setEnabled(enabled); + mRefreshEmptyLayout.setEnabled(enabled); } /** @@ -307,11 +339,71 @@ implements OnItemClickListener, OnEnforceableRefreshListener { @Override public void onRefresh(boolean ignoreETag) { - mRefreshLayout.setRefreshing(false); + mRefreshListLayout.setRefreshing(false); + mRefreshGridLayout.setRefreshing(false); mRefreshEmptyLayout.setRefreshing(false); if (mOnRefreshListener != null) { mOnRefreshListener.onRefresh(ignoreETag); } } + + + protected void setChoiceMode(int choiceMode) { + mListView.setChoiceMode(choiceMode); + mGridView.setChoiceMode(choiceMode); + } + + protected void registerForContextMenu() { + registerForContextMenu(mListView); + registerForContextMenu(mGridView); + mListView.setOnCreateContextMenuListener(this); + mGridView.setOnCreateContextMenuListener(this); + } + + /** + * TODO doc + * To be called before setAdapter, or GridViewWithHeaderAndFooter will throw an exception + * + * @param enabled + */ + protected void setFooterEnabled(boolean enabled) { + if (enabled) { + if (mGridView.getFooterViewCount() == 0) { + if (mGridFooterView.getParent() != null ) { + ((ViewGroup) mGridFooterView.getParent()).removeView(mGridFooterView); + } + mGridView.addFooterView(mGridFooterView, null, false); + } + mGridFooterView.invalidate(); + + if (mListView.getFooterViewsCount() == 0) { + if (mListFooterView.getParent() != null ) { + ((ViewGroup) mListFooterView.getParent()).removeView(mListFooterView); + } + mListView.addFooterView(mListFooterView, null, false); + } + mListFooterView.invalidate(); + + } else { + mGridView.removeFooterView(mGridFooterView); + mListView.removeFooterView(mListFooterView); + } + } + + /** + * TODO doc + * @param text + */ + protected void setFooterText(String text) { + if (text != null && text.length() > 0) { + ((TextView)mListFooterView.findViewById(R.id.footerText)).setText(text); + ((TextView)mGridFooterView.findViewById(R.id.footerText)).setText(text); + setFooterEnabled(true); + + } else { + setFooterEnabled(false); + } + } + } diff --git a/src/com/owncloud/android/ui/fragment/FileDetailFragment.java b/src/com/owncloud/android/ui/fragment/FileDetailFragment.java index 2ff29250..6a19957b 100644 --- a/src/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/src/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -1,6 +1,10 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski + * @author David A. Velasco * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -52,9 +56,6 @@ import com.owncloud.android.utils.DisplayUtils; /** * This Fragment is used to display the details about a file. - * - * @author Bartek Przybylski - * @author David A. Velasco */ public class FileDetailFragment extends FileFragment implements OnClickListener { @@ -348,7 +349,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()) { @@ -396,7 +400,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener } ImageView iv = (ImageView) getView().findViewById(R.id.fdIcon); if (iv != null) { - iv.setImageResource(DisplayUtils.getResourceId(mimetype, filename)); + iv.setImageResource(DisplayUtils.getFileTypeIconId(mimetype, filename)); } } @@ -449,6 +453,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())) { @@ -532,9 +537,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener /** - * Helper class responsible for updating the progress bar shown for file uploading or downloading - * - * @author David A. Velasco + * Helper class responsible for updating the progress bar shown for file uploading or downloading */ private class ProgressListener implements OnDatatransferProgressListener { int mLastPercent = 0; diff --git a/src/com/owncloud/android/ui/fragment/FileFragment.java b/src/com/owncloud/android/ui/fragment/FileFragment.java index 3e6fa31c..87dca259 100644 --- a/src/com/owncloud/android/ui/fragment/FileFragment.java +++ b/src/com/owncloud/android/ui/fragment/FileFragment.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -30,9 +33,6 @@ import com.owncloud.android.ui.activity.ComponentsGetter; /** * Common methods for {@link Fragment}s containing {@link OCFile}s - * - * @author David A. Velasco - * */ public class FileFragment extends SherlockFragment { @@ -102,8 +102,6 @@ public class FileFragment extends SherlockFragment { /** * Interface to implement by any Activity that includes some instance of FileListFragment * Interface to implement by any Activity that includes some instance of FileFragment - * - * @author David A. Velasco */ public interface ContainerActivity extends ComponentsGetter { diff --git a/src/com/owncloud/android/ui/fragment/LocalFileListFragment.java b/src/com/owncloud/android/ui/fragment/LocalFileListFragment.java index c9408b1e..b3e40d8d 100644 --- a/src/com/owncloud/android/ui/fragment/LocalFileListFragment.java +++ b/src/com/owncloud/android/ui/fragment/LocalFileListFragment.java @@ -1,6 +1,9 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author David A. Velasco * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -38,9 +41,6 @@ import com.owncloud.android.ui.adapter.LocalFileListAdapter; /** * A Fragment that lists all files and folders in a given LOCAL path. - * - * @author David A. Velasco - * */ public class LocalFileListFragment extends ExtendedListFragment { private static final String TAG = "LocalFileListFragment"; @@ -76,12 +76,12 @@ public class LocalFileListFragment extends ExtendedListFragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log_OC.i(TAG, "onCreateView() start"); View v = super.onCreateView(inflater, container, savedInstanceState); - getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - disableSwipe(); // Disable pull refresh + setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + setSwipeEnabled(false); // Disable pull-to-refresh setMessageForEmptyList(getString(R.string.local_file_list_empty)); Log_OC.i(TAG, "onCreateView() end"); return v; - } + } /** @@ -98,7 +98,6 @@ public class LocalFileListFragment extends ExtendedListFragment { Log_OC.i(TAG, "onActivityCreated() stop"); } - /** * Checks the file clicked over. Browses inside if it is a directory. Notifies the container activity in any case. */ @@ -195,10 +194,10 @@ public class LocalFileListFragment extends ExtendedListFragment { directory = directory.getParentFile(); } - mList.clearChoices(); // by now, only files in the same directory will be kept as selected + mCurrentListView.clearChoices(); // by now, only files in the same directory will be kept as selected mAdapter.swapDirectory(directory); if (mDirectory == null || !mDirectory.equals(directory)) { - mList.setSelectionFromTop(0, 0); + mCurrentListView.setSelection(0); } mDirectory = directory; } @@ -211,11 +210,11 @@ public class LocalFileListFragment extends ExtendedListFragment { */ public String[] getCheckedFilePaths() { ArrayList result = new ArrayList(); - SparseBooleanArray positions = mList.getCheckedItemPositions(); + SparseBooleanArray positions = mCurrentListView.getCheckedItemPositions(); if (positions.size() > 0) { for (int i = 0; i < positions.size(); i++) { if (positions.get(positions.keyAt(i)) == true) { - result.add(((File) mList.getItemAtPosition(positions.keyAt(i))).getAbsolutePath()); + result.add(((File) mCurrentListView.getItemAtPosition(positions.keyAt(i))).getAbsolutePath()); } } @@ -227,15 +226,13 @@ public class LocalFileListFragment extends ExtendedListFragment { /** * Interface to implement by any Activity that includes some instance of LocalFileListFragment - * - * @author David A. Velasco */ public interface ContainerActivity { /** * Callback method invoked when a directory is clicked by the user on the files list * - * @param file + * @param directory */ public void onDirectoryClick(File directory); diff --git a/src/com/owncloud/android/ui/fragment/OCFileListFragment.java b/src/com/owncloud/android/ui/fragment/OCFileListFragment.java index fd0b1a55..60ac78d9 100644 --- a/src/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/src/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -1,6 +1,11 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski + * @author masensio + * @author David A. Velasco * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2014 ownCloud Inc. + * 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, @@ -18,10 +23,8 @@ package com.owncloud.android.ui.fragment; import java.io.File; -import java.util.Vector; import android.app.Activity; -import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v4.widget.SwipeRefreshLayout; @@ -31,8 +34,6 @@ import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; -import android.widget.TextView; -import android.view.LayoutInflater; import com.owncloud.android.R; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -48,15 +49,12 @@ import com.owncloud.android.ui.dialog.RemoveFileDialogFragment; import com.owncloud.android.ui.dialog.RenameFileDialogFragment; import com.owncloud.android.ui.preview.PreviewImageFragment; import com.owncloud.android.ui.preview.PreviewMediaFragment; +import com.owncloud.android.utils.FileStorageUtils; /** * A Fragment that lists all files and folders in a given path. * * TODO refactorize to get rid of direct dependency on FileDisplayActivity - * - * @author Bartek Przybylski - * @author masensio - * @author David A. Velasco */ public class OCFileListFragment extends ExtendedListFragment { @@ -70,11 +68,13 @@ public class OCFileListFragment extends ExtendedListFragment { private static final String KEY_FILE = MY_PACKAGE + ".extra.FILE"; + private final static Double THUMBNAIL_THRESHOLD = 0.5; + private FileFragment.ContainerActivity mContainerActivity; private OCFile mFile = null; private FileListListAdapter mAdapter; - private View mFooterView; + private boolean mJustFolders; private OCFile mTargetFile; @@ -122,22 +122,23 @@ public class OCFileListFragment extends ExtendedListFragment { mFile = savedInstanceState.getParcelable(KEY_FILE); } - mFooterView = ((LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate( - R.layout.list_footer, null, false); - setFooterView(mFooterView); + if (mJustFolders) { + setFooterEnabled(false); + } else { + setFooterEnabled(true); + } Bundle args = getArguments(); - boolean justFolders = (args == null) ? false : args.getBoolean(ARG_JUST_FOLDERS, false); + mJustFolders = (args == null) ? false : args.getBoolean(ARG_JUST_FOLDERS, false); mAdapter = new FileListListAdapter( - justFolders, - getSherlockActivity(), + mJustFolders, + getSherlockActivity(), mContainerActivity ); setListAdapter(mAdapter); - registerForContextMenu(getListView()); - getListView().setOnCreateContextMenuListener(this); - } + registerForContextMenu(); + } /** * Saves the current listed folder. @@ -257,15 +258,9 @@ public class OCFileListFragment extends ExtendedListFragment { ); mf.filter(menu); } - - /// additional restrictions for this fragment - // TODO allow in the future 'open with' for previewable files - MenuItem item = menu.findItem(R.id.action_open_file_with); - if (item != null) { - item.setVisible(false); - item.setEnabled(false); - } + /// TODO break this direct dependency on FileDisplayActivity... if possible + MenuItem item = menu.findItem(R.id.action_open_file_with); FileFragment frag = ((FileDisplayActivity)getSherlockActivity()).getSecondFragment(); if (frag != null && frag instanceof FileDetailFragment && frag.getFile().getFileId() == targetFile.getFileId()) { @@ -291,6 +286,10 @@ public class OCFileListFragment extends ExtendedListFragment { mContainerActivity.getFileOperationsHelper().shareFileWithLink(mTargetFile); return true; } + case R.id.action_open_file_with: { + mContainerActivity.getFileOperationsHelper().openFile(mTargetFile); + return true; + } case R.id.action_unshare_file: { mContainerActivity.getFileOperationsHelper().unshareFileWithLink(mTargetFile); return true; @@ -390,64 +389,75 @@ public class OCFileListFragment extends ExtendedListFragment { mAdapter.swapDirectory(directory, storageManager); if (mFile == null || !mFile.equals(directory)) { - mList.setSelectionFromTop(0, 0); + mCurrentListView.setSelection(0); } mFile = directory; - - // Update Footer - TextView footerText = (TextView) mFooterView.findViewById(R.id.footerText); - Log_OC.d("footer", String.valueOf(System.currentTimeMillis())); - footerText.setText(generateFooterText(directory)); - Log_OC.d("footer", String.valueOf(System.currentTimeMillis())); + + updateLayout(); + } } - - private String generateFooterText(OCFile directory) { - Integer files = 0; - Integer folders = 0; - FileDataStorageManager storageManager = mContainerActivity.getStorageManager(); - Vector mFiles = storageManager.getFolderContent(mFile); + private void updateLayout() { + if (!mJustFolders) { + int filesCount = 0, foldersCount = 0, imagesCount = 0; + int count = mAdapter.getCount(); + OCFile file; + for (int i=0; i < count ; i++) { + file = (OCFile) mAdapter.getItem(i); + if (file.isFolder()) { + foldersCount++; + } else { + filesCount++; + if (file.isImage()){ + imagesCount++; + } + } + } + // set footer text + setFooterText(generateFooterText(filesCount, foldersCount)); - for (OCFile ocFile : mFiles) { - if (ocFile.isFolder()) { - folders++; + // decide grid vs list view + if (((double)imagesCount / (double)filesCount) >= THUMBNAIL_THRESHOLD) { + switchToGridView(); } else { - files++; + switchToListView(); } } + } + private String generateFooterText(int filesCount, int foldersCount) { String output = ""; - - if (files > 0){ - if (files == 1) { - output = output + files.toString() + " " + getResources().getString(R.string.file_list_file); + if (filesCount > 0){ + if (filesCount == 1) { + output = output + filesCount + " " + getResources().getString(R.string.file_list_file); } else { - output = output + files.toString() + " " + getResources().getString(R.string.file_list_files); + output = output + filesCount + " " + getResources().getString(R.string.file_list_files); } } - if (folders > 0 && files > 0){ + if (foldersCount > 0 && filesCount > 0){ output = output + ", "; } - if (folders == 1) { - output = output + folders.toString() + " " + getResources().getString(R.string.file_list_folder); - } else if (folders > 1) { - output = output + folders.toString() + " " + getResources().getString(R.string.file_list_folders); + if (foldersCount == 1) { + output = output + foldersCount + " " + getResources().getString(R.string.file_list_folder); + } else if (foldersCount > 1) { + output = output + foldersCount + " " + getResources().getString(R.string.file_list_folders); } - + return output; } - + + public void sortByName(boolean descending) { - mAdapter.setSortOrder(FileListListAdapter.SORT_NAME, descending); + mAdapter.setSortOrder(FileStorageUtils.SORT_NAME, descending); } public void sortByDate(boolean descending) { - mAdapter.setSortOrder(FileListListAdapter.SORT_DATE, descending); + mAdapter.setSortOrder(FileStorageUtils.SORT_DATE, descending); } public void sortBySize(boolean descending) { - mAdapter.setSortOrder(FileListListAdapter.SORT_SIZE, descending); + mAdapter.setSortOrder(FileStorageUtils.SORT_SIZE, descending); } } diff --git a/src/com/owncloud/android/ui/preview/FileDownloadFragment.java b/src/com/owncloud/android/ui/preview/FileDownloadFragment.java index 98bbda38..99a4d444 100644 --- a/src/com/owncloud/android/ui/preview/FileDownloadFragment.java +++ b/src/com/owncloud/android/ui/preview/FileDownloadFragment.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application - * - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -42,8 +44,6 @@ import com.owncloud.android.lib.common.utils.Log_OC; /** * This Fragment is used to monitor the progress of a file downloading. - * - * @author David A. Velasco */ public class FileDownloadFragment extends FileFragment implements OnClickListener { @@ -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 @@ -307,9 +308,7 @@ public class FileDownloadFragment extends FileFragment implements OnClickListene /** - * Helper class responsible for updating the progress bar shown for file uploading or downloading - * - * @author David A. Velasco + * Helper class responsible for updating the progress bar shown for file uploading or downloading */ private class ProgressListener implements OnDatatransferProgressListener { int mLastPercent = 0; diff --git a/src/com/owncloud/android/ui/preview/PreviewImageActivity.java b/src/com/owncloud/android/ui/preview/PreviewImageActivity.java index 1cee30e8..1f6ab809 100644 --- a/src/com/owncloud/android/ui/preview/PreviewImageActivity.java +++ b/src/com/owncloud/android/ui/preview/PreviewImageActivity.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -62,8 +65,6 @@ import com.owncloud.android.utils.DisplayUtils; /** * Holds a swiping galley where image files contained in an ownCloud directory are shown - * - * @author David A. Velasco */ public class PreviewImageActivity extends FileActivity implements FileFragment.ContainerActivity, @@ -426,7 +427,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. diff --git a/src/com/owncloud/android/ui/preview/PreviewImageFragment.java b/src/com/owncloud/android/ui/preview/PreviewImageFragment.java index 0995793d..9d1cd60f 100644 --- a/src/com/owncloud/android/ui/preview/PreviewImageFragment.java +++ b/src/com/owncloud/android/ui/preview/PreviewImageFragment.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -54,8 +57,8 @@ import com.owncloud.android.ui.dialog.ConfirmationDialogFragment; import com.owncloud.android.ui.dialog.RemoveFileDialogFragment; import com.owncloud.android.ui.fragment.FileFragment; import com.owncloud.android.utils.BitmapUtils; -import com.owncloud.android.utils.TouchImageViewCustom; +import third_parties.michaelOrtiz.TouchImageViewCustom; /** @@ -64,8 +67,6 @@ import com.owncloud.android.utils.TouchImageViewCustom; * Trying to get an instance with NULL {@link OCFile} or ownCloud {@link Account} values will produce an {@link IllegalStateException}. * * If the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on instantiation too. - * - * @author David A. Velasco */ public class PreviewImageFragment extends FileFragment { diff --git a/src/com/owncloud/android/ui/preview/PreviewImagePagerAdapter.java b/src/com/owncloud/android/ui/preview/PreviewImagePagerAdapter.java index f2a9a9b2..14ae34fa 100644 --- a/src/com/owncloud/android/ui/preview/PreviewImagePagerAdapter.java +++ b/src/com/owncloud/android/ui/preview/PreviewImagePagerAdapter.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -16,6 +19,8 @@ */ package com.owncloud.android.ui.preview; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -31,12 +36,12 @@ import android.view.ViewGroup; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.ui.adapter.FileListListAdapter; import com.owncloud.android.ui.fragment.FileFragment; +import com.owncloud.android.utils.FileStorageUtils; /** - * Adapter class that provides Fragment instances - * - * @author David A. Velasco + * Adapter class that provides Fragment instances */ //public class PreviewImagePagerAdapter extends PagerAdapter { public class PreviewImagePagerAdapter extends FragmentStatePagerAdapter { @@ -73,13 +78,15 @@ public class PreviewImagePagerAdapter extends FragmentStatePagerAdapter { mAccount = account; mStorageManager = storageManager; mImageFiles = mStorageManager.getFolderImages(parentFolder); + + mImageFiles = FileStorageUtils.sortFolder(mImageFiles); + mObsoleteFragments = new HashSet(); mObsoletePositions = new HashSet(); mDownloadErrors = new HashSet(); //mFragmentManager = fragmentManager; mCachedFragments = new HashMap(); } - /** * Returns the image files handled by the adapter. diff --git a/src/com/owncloud/android/ui/preview/PreviewMediaFragment.java b/src/com/owncloud/android/ui/preview/PreviewMediaFragment.java index 7d6489b2..d82faa59 100644 --- a/src/com/owncloud/android/ui/preview/PreviewMediaFragment.java +++ b/src/com/owncloud/android/ui/preview/PreviewMediaFragment.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -64,8 +67,6 @@ import com.owncloud.android.ui.fragment.FileFragment; * Trying to get an instance with NULL {@link OCFile} or ownCloud {@link Account} values will produce an {@link IllegalStateException}. * * By now, if the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on instantiation too. - * - * @author David A. Velasco */ public class PreviewMediaFragment extends FileFragment implements OnTouchListener { diff --git a/src/com/owncloud/android/ui/preview/PreviewVideoActivity.java b/src/com/owncloud/android/ui/preview/PreviewVideoActivity.java index 39e8e234..938d52d4 100644 --- a/src/com/owncloud/android/ui/preview/PreviewVideoActivity.java +++ b/src/com/owncloud/android/ui/preview/PreviewVideoActivity.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -45,9 +48,7 @@ import com.owncloud.android.lib.common.utils.Log_OC; * Used as an utility to preview video files contained in an ownCloud account. * * Currently, it always plays in landscape mode, full screen. When the playback ends, - * the activity is finished. - * - * @author David A. Velasco + * the activity is finished. */ public class PreviewVideoActivity extends FileActivity implements OnCompletionListener, OnPreparedListener, OnErrorListener { diff --git a/src/com/owncloud/android/utils/BitmapUtils.java b/src/com/owncloud/android/utils/BitmapUtils.java index 70367278..ce7590d5 100644 --- a/src/com/owncloud/android/utils/BitmapUtils.java +++ b/src/com/owncloud/android/utils/BitmapUtils.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -23,11 +26,13 @@ import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.graphics.BitmapFactory.Options; import android.media.ExifInterface; +import android.net.Uri; +import android.webkit.MimeTypeMap; + +import java.io.File; /** * Utility class with methods for decoding Bitmaps. - * - * @author David A. Velasco */ public class BitmapUtils { @@ -169,6 +174,18 @@ public class BitmapUtils { } return resultBitmap; } - + + /** + * Checks if file passed is an image + * @param file + * @return true/false + */ + public static boolean isImage(File file) { + Uri selectedUri = Uri.fromFile(file); + String fileExtension = MimeTypeMap.getFileExtensionFromUrl(selectedUri.toString().toLowerCase()); + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension); + + return (mimeType != null && mimeType.startsWith("image/")); + } } diff --git a/src/com/owncloud/android/utils/DisplayUtils.java b/src/com/owncloud/android/utils/DisplayUtils.java index a1afb894..905f60b2 100644 --- a/src/com/owncloud/android/utils/DisplayUtils.java +++ b/src/com/owncloud/android/utils/DisplayUtils.java @@ -1,6 +1,10 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski + * @author David A. Velasco * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -19,17 +23,20 @@ package com.owncloud.android.utils; import java.net.IDN; +import java.text.DateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Set; +import java.util.Vector; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.text.format.DateUtils; +import android.webkit.MimeTypeMap; import com.owncloud.android.MainApp; import com.owncloud.android.R; @@ -37,9 +44,6 @@ import com.owncloud.android.datamodel.OCFile; /** * A helper class for some string operations. - * - * @author Bartek Przybylski - * @author David A. Velasco */ public class DisplayUtils { @@ -73,21 +77,28 @@ public class DisplayUtils { private static final String TYPE_VIDEO = "video"; private static final String SUBTYPE_PDF = "pdf"; - private static final String[] SUBTYPES_DOCUMENT = { "msword", - "vnd.openxmlformats-officedocument.wordprocessingml.document", - "vnd.oasis.opendocument.text", - "rtf" - }; + private static final String SUBTYPE_XML = "xml"; + private static final String[] SUBTYPES_DOCUMENT = { + "msword", + "vnd.openxmlformats-officedocument.wordprocessingml.document", + "vnd.oasis.opendocument.text", + "rtf", + "javascript" + }; private static Set SUBTYPES_DOCUMENT_SET = new HashSet(Arrays.asList(SUBTYPES_DOCUMENT)); - private static final String[] SUBTYPES_SPREADSHEET = { "msexcel", - "vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "vnd.oasis.opendocument.spreadsheet" - }; + private static final String[] SUBTYPES_SPREADSHEET = { + "msexcel", + "vnd.ms-excel", + "vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "vnd.oasis.opendocument.spreadsheet" + }; private static Set SUBTYPES_SPREADSHEET_SET = new HashSet(Arrays.asList(SUBTYPES_SPREADSHEET)); - private static final String[] SUBTYPES_PRESENTATION = { "mspowerpoint", - "vnd.openxmlformats-officedocument.presentationml.presentation", - "vnd.oasis.opendocument.presentation" - }; + private static final String[] SUBTYPES_PRESENTATION = { + "mspowerpoint", + "vnd.ms-powerpoint", + "vnd.openxmlformats-officedocument.presentationml.presentation", + "vnd.oasis.opendocument.presentation" + }; private static Set SUBTYPES_PRESENTATION_SET = new HashSet(Arrays.asList(SUBTYPES_PRESENTATION)); private static final String[] SUBTYPES_COMPRESSED = {"x-tar", "x-gzip", "zip"}; private static final Set SUBTYPES_COMPRESSED_SET = new HashSet(Arrays.asList(SUBTYPES_COMPRESSED)); @@ -95,6 +106,8 @@ public class DisplayUtils { private static final String EXTENSION_RAR = "rar"; private static final String EXTENSION_RTF = "rtf"; private static final String EXTENSION_3GP = "3gp"; + private static final String EXTENSION_PY = "py"; + private static final String EXTENSION_JS = "js"; /** * Converts the file size in bytes to human readable output. @@ -114,30 +127,6 @@ public class DisplayUtils { } /** - * Removes special HTML entities from a string - * - * @param s Input string - * @return A cleaned version of the string - */ - public static String HtmlDecode(String s) { - /* - * TODO: Perhaps we should use something more proven like: - * http://commons.apache.org/lang/api-2.6/org/apache/commons/lang/StringEscapeUtils.html#unescapeHtml%28java.lang.String%29 - */ - - String ret = ""; - for (int i = 0; i < s.length(); ++i) { - if (s.charAt(i) == '%') { - ret += (char) Integer.parseInt(s.substring(i + 1, i + 3), 16); - i += 2; - } else { - ret += s.charAt(i); - } - } - return ret; - } - - /** * Converts MIME types like "image/jpg" to more end user friendly output * like "JPG image". * @@ -155,18 +144,25 @@ public class DisplayUtils { /** - * Returns the resource identifier of an image resource to use as icon associated to a - * known MIME type. + * Returns the resource identifier of an image to use as icon associated to a known MIME type. * - * @param mimetype MIME type string. - * @param filename name, with extension - * @return Resource identifier of an image resource. + * @param mimetype MIME type string; if NULL, the method tries to guess it from the extension in filename + * @param filename Name, with extension. + * @return Identifier of an image resource. */ - public static int getResourceId(String mimetype, String filename) { + public static int getFileTypeIconId(String mimetype, String filename) { - if (mimetype == null || "DIR".equals(mimetype)) { - return R.drawable.ic_menu_archive; + if (mimetype == null) { + String fileExtension = getExtension(filename); + mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension); + if (mimetype == null) { + mimetype = TYPE_APPLICATION + "/" + SUBTYPE_OCTET_STREAM; + } + } + if ("DIR".equals(mimetype)) { + return R.drawable.ic_menu_archive; + } else { String [] parts = mimetype.split("/"); String type = parts[0]; @@ -189,6 +185,9 @@ public class DisplayUtils { if (SUBTYPE_PDF.equals(subtype)) { return R.drawable.file_pdf; + } else if (SUBTYPE_XML.equals(subtype)) { + return R.drawable.file_doc; + } else if (SUBTYPES_DOCUMENT_SET.contains(subtype)) { return R.drawable.file_doc; @@ -200,7 +199,7 @@ public class DisplayUtils { } else if (SUBTYPES_COMPRESSED_SET.contains(subtype)) { return R.drawable.file_zip; - + } else if (SUBTYPE_OCTET_STREAM.equals(subtype) ) { if (getExtension(filename).equalsIgnoreCase(EXTENSION_RAR)) { return R.drawable.file_zip; @@ -210,7 +209,10 @@ public class DisplayUtils { } else if (getExtension(filename).equalsIgnoreCase(EXTENSION_3GP)) { return R.drawable.file_movie; - + + } else if ( getExtension(filename).equalsIgnoreCase(EXTENSION_PY) || + getExtension(filename).equalsIgnoreCase(EXTENSION_JS)) { + return R.drawable.file_doc; } } } @@ -222,19 +224,19 @@ public class DisplayUtils { private static String getExtension(String filename) { - String extension = filename.substring(filename.lastIndexOf(".") + 1); - + String extension = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase(); return extension; } /** * Converts Unix time to human readable format - * @param miliseconds that have passed since 01/01/1970 + * @param milliseconds that have passed since 01/01/1970 * @return The human readable time for the users locale */ public static String unixTimeToHumanReadable(long milliseconds) { Date date = new Date(milliseconds); - return date.toLocaleString(); + DateFormat df = DateFormat.getDateTimeInstance(); + return df.format(date); } @@ -295,7 +297,11 @@ public class DisplayUtils { return fileExtension; } - public static CharSequence getRelativeDateTimeString(Context c, long time, long minResolution, long transitionResolution, int flags){ + @SuppressWarnings("deprecation") + public static CharSequence getRelativeDateTimeString ( + Context c, long time, long minResolution, long transitionResolution, int flags + ){ + CharSequence dateString = ""; // in Future @@ -307,18 +313,21 @@ public class DisplayUtils { return c.getString(R.string.file_list_seconds_ago); } else { // Workaround 2.x bug (see https://github.com/owncloud/android/issues/716) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB && (System.currentTimeMillis() - time) > 24 * 60 * 60 * 1000){ + if ( Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB && + (System.currentTimeMillis() - time) > 24 * 60 * 60 * 1000 ) { Date date = new Date(time); date.setHours(0); date.setMinutes(0); date.setSeconds(0); - dateString = DateUtils.getRelativeDateTimeString(c, date.getTime(), minResolution, transitionResolution, flags); + dateString = DateUtils.getRelativeDateTimeString( + c, date.getTime(), minResolution, transitionResolution, flags + ); } else { dateString = DateUtils.getRelativeDateTimeString(c, time, minResolution, transitionResolution, flags); } } - return dateString.toString().split(",")[0]; + return dateString.toString().split(",")[0]; } /** @@ -333,4 +342,5 @@ public class DisplayUtils { } return path; } + } diff --git a/src/com/owncloud/android/utils/ErrorMessageAdapter.java b/src/com/owncloud/android/utils/ErrorMessageAdapter.java index e56e8760..12a1a5ac 100644 --- a/src/com/owncloud/android/utils/ErrorMessageAdapter.java +++ b/src/com/owncloud/android/utils/ErrorMessageAdapter.java @@ -1,4 +1,7 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author masensio * Copyright (C) 2014 ownCloud Inc. * * This program is free software: you can redistribute it and/or modify @@ -36,14 +39,13 @@ 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; /** - * Class to choose proper error messages to show to the user depending on the results of operations, always following the same policy - * - * @author masensio - * + * Class to choose proper error messages to show to the user depending on the results of operations, + * always following the same policy */ public class ErrorMessageAdapter { @@ -206,6 +208,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; diff --git a/src/com/owncloud/android/utils/FileStorageUtils.java b/src/com/owncloud/android/utils/FileStorageUtils.java index 892a1ca8..e70302fc 100644 --- a/src/com/owncloud/android/utils/FileStorageUtils.java +++ b/src/com/owncloud/android/utils/FileStorageUtils.java @@ -1,5 +1,8 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. +/** + * ownCloud Android client application + * + * @author David A. Velasco + * 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, @@ -18,6 +21,11 @@ package com.owncloud.android.utils; import java.io.File; +import java.util.Collections; +import java.util.Comparator; +import java.util.Vector; + +import third_parties.daveKoeller.AlphanumComparator; import com.owncloud.android.MainApp; import com.owncloud.android.R; @@ -31,14 +39,20 @@ import android.preference.PreferenceManager; import android.net.Uri; import android.os.Environment; import android.os.StatFs; +import android.webkit.MimeTypeMap; /** * Static methods to help in access to local file system. - * - * @author David A. Velasco */ public class FileStorageUtils { + public static Integer mSortOrder; + public static Boolean mSortAscending; + public static final Integer SORT_NAME = 0; + public static final Integer SORT_DATE = 1; + public static final Integer SORT_SIZE = 2; + + //private static final String LOG_TAG = "FileStorageUtils"; public static final String getSavePath(String accountName) { @@ -123,7 +137,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){ @@ -137,5 +151,156 @@ public class FileStorageUtils { file.setRemoteId(ocFile.getRemoteId()); return file; } + + /** + * Sorts all filenames, regarding last user decision + */ + public static Vector sortFolder(Vector files){ + switch (mSortOrder){ + case 0: + files = FileStorageUtils.sortByName(files); + break; + case 1: + files = FileStorageUtils.sortByDate(files); + break; + case 2: + // mFiles = FileStorageUtils.sortBySize(mSortAscending); + break; + } + + return files; + } + + /** + * Sorts list by Date + * @param files + */ + public static Vector sortByDate(Vector files){ + final Integer val; + if (mSortAscending){ + val = 1; + } else { + val = -1; + } + + Collections.sort(files, new Comparator() { + 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()); + } + } + }); + + return files; + } + +// /** +// * Sorts list by Size +// * @param sortAscending true: ascending, false: descending +// */ +// public static Vector sortBySize(Vector files){ +// final Integer val; +// if (mSortAscending){ +// val = 1; +// } else { +// val = -1; +// } +// +// Collections.sort(files, new Comparator() { +// 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()); +// } +// } +// }); +// +// return files; +// } + + /** + * Sorts list by Name + * @param files files to sort + */ + public static Vector sortByName(Vector files){ + final Integer val; + if (mSortAscending){ + val = 1; + } else { + val = -1; + } + + Collections.sort(files, new Comparator() { + 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); + } + }); + + return files; + } + + /** + * Local Folder size + * @param dir File + * @return Size in bytes + */ + public static long getFolderSize(File dir) { + if (dir.exists()) { + long result = 0; + File[] fileList = dir.listFiles(); + for(int i = 0; i < fileList.length; i++) { + if(fileList[i].isDirectory()) { + result += getFolderSize(fileList[i]); + } else { + result += fileList[i].length(); + } + } + 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 : ""; + } } diff --git a/src/com/owncloud/android/utils/OwnCloudSession.java b/src/com/owncloud/android/utils/OwnCloudSession.java index 13ead88b..6292a2b2 100644 --- a/src/com/owncloud/android/utils/OwnCloudSession.java +++ b/src/com/owncloud/android/utils/OwnCloudSession.java @@ -1,6 +1,9 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * + * @author Bartek Przybylski * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, @@ -19,9 +22,6 @@ package com.owncloud.android.utils; /** * Represents a session to an ownCloud instance - * - * @author Bartek Przybylski - * */ public class OwnCloudSession { private String mSessionName; diff --git a/src/com/owncloud/android/utils/RecursiveFileObserver.java b/src/com/owncloud/android/utils/RecursiveFileObserver.java index be44f8f6..4a70631a 100644 --- a/src/com/owncloud/android/utils/RecursiveFileObserver.java +++ b/src/com/owncloud/android/utils/RecursiveFileObserver.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, diff --git a/src/com/owncloud/android/utils/TouchImageViewCustom.java b/src/com/owncloud/android/utils/TouchImageViewCustom.java deleted file mode 100644 index a0f7b792..00000000 --- a/src/com/owncloud/android/utils/TouchImageViewCustom.java +++ /dev/null @@ -1,1276 +0,0 @@ -/* - * TouchImageView.java - * By: Michael Ortiz - * Updated By: Patrick Lackemacher - * Updated By: Babay88 - * Updated By: @ipsilondev - * Updated By: hank-cp - * Updated By: singpolyma - * ------------------- - * Extends Android ImageView to include pinch zooming, panning, fling and double tap zoom. - */ - -package com.owncloud.android.utils; - -import com.owncloud.android.ui.preview.ImageViewCustom; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.PointF; -import android.graphics.RectF; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Build; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; -import android.os.Bundle; -import android.os.Parcelable; -import android.util.AttributeSet; -import android.util.Log; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.view.ScaleGestureDetector; -import android.view.View; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.widget.OverScroller; -import android.widget.Scroller; - -public class TouchImageViewCustom extends ImageViewCustom { - private static final String DEBUG = "DEBUG"; - - // - // SuperMin and SuperMax multipliers. Determine how much the image can be - // zoomed below or above the zoom boundaries, before animating back to the - // min/max zoom boundary. - // - private static final float SUPER_MIN_MULTIPLIER = .75f; - private static final float SUPER_MAX_MULTIPLIER = 1.25f; - - // - // Scale of image ranges from minScale to maxScale, where minScale == 1 - // when the image is stretched to fit view. - // - private float normalizedScale; - - // - // Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal. - // MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix - // saved prior to the screen rotating. - // - private Matrix matrix, prevMatrix; - - private static enum State { NONE, DRAG, ZOOM, FLING, ANIMATE_ZOOM }; - private State state; - - private float minScale; - private float maxScale; - private float superMinScale; - private float superMaxScale; - private float[] m; - - private Context context; - private Fling fling; - - private ScaleType mScaleType; - - private boolean imageRenderedAtLeastOnce; - private boolean onDrawReady; - - private ZoomVariables delayedZoomVariables; - - // - // Size of view and previous view size (ie before rotation) - // - private int viewWidth, viewHeight, prevViewWidth, prevViewHeight; - - // - // Size of image when it is stretched to fit view. Before and After rotation. - // - private float matchViewWidth, matchViewHeight, prevMatchViewWidth, prevMatchViewHeight; - - private ScaleGestureDetector mScaleDetector; - private GestureDetector mGestureDetector; - private GestureDetector.OnDoubleTapListener doubleTapListener = null; - private OnTouchListener userTouchListener = null; - private OnTouchImageViewListener touchImageViewListener = null; - - public TouchImageViewCustom(Context context) { - super(context); - sharedConstructing(context); - } - - public TouchImageViewCustom(Context context, AttributeSet attrs) { - super(context, attrs); - sharedConstructing(context); - } - - public TouchImageViewCustom(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - sharedConstructing(context); - } - - private void sharedConstructing(Context context) { - super.setClickable(true); - this.context = context; - mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); - mGestureDetector = new GestureDetector(context, new GestureListener()); - matrix = new Matrix(); - prevMatrix = new Matrix(); - m = new float[9]; - normalizedScale = 1; - if (mScaleType == null) { - mScaleType = ScaleType.FIT_CENTER; - } - minScale = 1; - maxScale = 3; - superMinScale = SUPER_MIN_MULTIPLIER * minScale; - superMaxScale = SUPER_MAX_MULTIPLIER * maxScale; - setImageMatrix(matrix); - setScaleType(ScaleType.MATRIX); - setState(State.NONE); - onDrawReady = false; - super.setOnTouchListener(new PrivateOnTouchListener()); - } - - @Override - public void setOnTouchListener(View.OnTouchListener l) { - userTouchListener = l; - } - - public void setOnTouchImageViewListener(OnTouchImageViewListener l) { - touchImageViewListener = l; - } - - public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener l) { - doubleTapListener = l; - } - - @Override - public void setImageResource(int resId) { - super.setImageResource(resId); - savePreviousImageValues(); - fitImageToView(); - } - - @Override - public void setImageBitmap(Bitmap bm) { - super.setImageBitmap(bm); - savePreviousImageValues(); - fitImageToView(); - } - - @Override - public void setImageDrawable(Drawable drawable) { - super.setImageDrawable(drawable); - savePreviousImageValues(); - fitImageToView(); - } - - @Override - public void setImageURI(Uri uri) { - super.setImageURI(uri); - savePreviousImageValues(); - fitImageToView(); - } - - @Override - public void setScaleType(ScaleType type) { - if (type == ScaleType.FIT_START || type == ScaleType.FIT_END) { - throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END"); - } - if (type == ScaleType.MATRIX) { - super.setScaleType(ScaleType.MATRIX); - - } else { - mScaleType = type; - if (onDrawReady) { - // - // If the image is already rendered, scaleType has been called programmatically - // and the TouchImageView should be updated with the new scaleType. - // - setZoom(this); - } - } - } - - @Override - public ScaleType getScaleType() { - return mScaleType; - } - - /** - * Returns false if image is in initial, unzoomed state. False, otherwise. - * @return true if image is zoomed - */ - public boolean isZoomed() { - return normalizedScale != 1; - } - - /** - * Return a Rect representing the zoomed image. - * @return rect representing zoomed image - */ - public RectF getZoomedRect() { - if (mScaleType == ScaleType.FIT_XY) { - throw new UnsupportedOperationException("getZoomedRect() not supported with FIT_XY"); - } - PointF topLeft = transformCoordTouchToBitmap(0, 0, true); - PointF bottomRight = transformCoordTouchToBitmap(viewWidth, viewHeight, true); - - float w = getDrawable().getIntrinsicWidth(); - float h = getDrawable().getIntrinsicHeight(); - return new RectF(topLeft.x / w, topLeft.y / h, bottomRight.x / w, bottomRight.y / h); - } - - /** - * Save the current matrix and view dimensions - * in the prevMatrix and prevView variables. - */ - private void savePreviousImageValues() { - if (matrix != null && viewHeight != 0 && viewWidth != 0) { - matrix.getValues(m); - prevMatrix.setValues(m); - prevMatchViewHeight = matchViewHeight; - prevMatchViewWidth = matchViewWidth; - prevViewHeight = viewHeight; - prevViewWidth = viewWidth; - } - } - - @Override - public Parcelable onSaveInstanceState() { - Bundle bundle = new Bundle(); - bundle.putParcelable("instanceState", super.onSaveInstanceState()); - bundle.putFloat("saveScale", normalizedScale); - bundle.putFloat("matchViewHeight", matchViewHeight); - bundle.putFloat("matchViewWidth", matchViewWidth); - bundle.putInt("viewWidth", viewWidth); - bundle.putInt("viewHeight", viewHeight); - matrix.getValues(m); - bundle.putFloatArray("matrix", m); - bundle.putBoolean("imageRendered", imageRenderedAtLeastOnce); - return bundle; - } - - @Override - public void onRestoreInstanceState(Parcelable state) { - if (state instanceof Bundle) { - Bundle bundle = (Bundle) state; - normalizedScale = bundle.getFloat("saveScale"); - m = bundle.getFloatArray("matrix"); - prevMatrix.setValues(m); - prevMatchViewHeight = bundle.getFloat("matchViewHeight"); - prevMatchViewWidth = bundle.getFloat("matchViewWidth"); - prevViewHeight = bundle.getInt("viewHeight"); - prevViewWidth = bundle.getInt("viewWidth"); - imageRenderedAtLeastOnce = bundle.getBoolean("imageRendered"); - super.onRestoreInstanceState(bundle.getParcelable("instanceState")); - return; - } - - super.onRestoreInstanceState(state); - } - - @Override - protected void onDraw(Canvas canvas) { - onDrawReady = true; - imageRenderedAtLeastOnce = true; - if (delayedZoomVariables != null) { - setZoom(delayedZoomVariables.scale, delayedZoomVariables.focusX, delayedZoomVariables.focusY, delayedZoomVariables.scaleType); - delayedZoomVariables = null; - } - super.onDraw(canvas); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - savePreviousImageValues(); - } - - /** - * Get the max zoom multiplier. - * @return max zoom multiplier. - */ - public float getMaxZoom() { - return maxScale; - } - - /** - * Set the max zoom multiplier. Default value: 3. - * @param max max zoom multiplier. - */ - public void setMaxZoom(float max) { - maxScale = max; - superMaxScale = SUPER_MAX_MULTIPLIER * maxScale; - } - - /** - * Get the min zoom multiplier. - * @return min zoom multiplier. - */ - public float getMinZoom() { - return minScale; - } - - /** - * Get the current zoom. This is the zoom relative to the initial - * scale, not the original resource. - * @return current zoom multiplier. - */ - public float getCurrentZoom() { - return normalizedScale; - } - - /** - * Set the min zoom multiplier. Default value: 1. - * @param min min zoom multiplier. - */ - public void setMinZoom(float min) { - minScale = min; - superMinScale = SUPER_MIN_MULTIPLIER * minScale; - } - - /** - * Reset zoom and translation to initial state. - */ - public void resetZoom() { - normalizedScale = 1; - fitImageToView(); - } - - /** - * Set zoom to the specified scale. Image will be centered by default. - * @param scale - */ - public void setZoom(float scale) { - setZoom(scale, 0.5f, 0.5f); - } - - /** - * Set zoom to the specified scale. Image will be centered around the point - * (focusX, focusY). These floats range from 0 to 1 and denote the focus point - * as a fraction from the left and top of the view. For example, the top left - * corner of the image would be (0, 0). And the bottom right corner would be (1, 1). - * @param scale - * @param focusX - * @param focusY - */ - public void setZoom(float scale, float focusX, float focusY) { - setZoom(scale, focusX, focusY, mScaleType); - } - - /** - * Set zoom to the specified scale. Image will be centered around the point - * (focusX, focusY). These floats range from 0 to 1 and denote the focus point - * as a fraction from the left and top of the view. For example, the top left - * corner of the image would be (0, 0). And the bottom right corner would be (1, 1). - * @param scale - * @param focusX - * @param focusY - * @param scaleType - */ - public void setZoom(float scale, float focusX, float focusY, ScaleType scaleType) { - // - // setZoom can be called before the image is on the screen, but at this point, - // image and view sizes have not yet been calculated in onMeasure. Thus, we should - // delay calling setZoom until the view has been measured. - // - if (!onDrawReady) { - delayedZoomVariables = new ZoomVariables(scale, focusX, focusY, scaleType); - return; - } - - if (scaleType != mScaleType) { - setScaleType(scaleType); - } - resetZoom(); - scaleImage(scale, viewWidth / 2, viewHeight / 2, true); - matrix.getValues(m); - m[Matrix.MTRANS_X] = -((focusX * getImageWidth()) - (viewWidth * 0.5f)); - m[Matrix.MTRANS_Y] = -((focusY * getImageHeight()) - (viewHeight * 0.5f)); - matrix.setValues(m); - fixTrans(); - setImageMatrix(matrix); - } - - /** - * Set zoom parameters equal to another TouchImageView. Including scale, position, - * and ScaleType. - * @param TouchImageView - */ - public void setZoom(TouchImageViewCustom img) { - PointF center = img.getScrollPosition(); - setZoom(img.getCurrentZoom(), center.x, center.y, img.getScaleType()); - } - - /** - * Return the point at the center of the zoomed image. The PointF coordinates range - * in value between 0 and 1 and the focus point is denoted as a fraction from the left - * and top of the view. For example, the top left corner of the image would be (0, 0). - * And the bottom right corner would be (1, 1). - * @return PointF representing the scroll position of the zoomed image. - */ - public PointF getScrollPosition() { - Drawable drawable = getDrawable(); - if (drawable == null) { - return null; - } - int drawableWidth = drawable.getIntrinsicWidth(); - int drawableHeight = drawable.getIntrinsicHeight(); - - PointF point = transformCoordTouchToBitmap(viewWidth / 2, viewHeight / 2, true); - point.x /= drawableWidth; - point.y /= drawableHeight; - return point; - } - - /** - * Set the focus point of the zoomed image. The focus points are denoted as a fraction from the - * left and top of the view. The focus points can range in value between 0 and 1. - * @param focusX - * @param focusY - */ - public void setScrollPosition(float focusX, float focusY) { - setZoom(normalizedScale, focusX, focusY); - } - - /** - * Performs boundary checking and fixes the image matrix if it - * is out of bounds. - */ - private void fixTrans() { - matrix.getValues(m); - float transX = m[Matrix.MTRANS_X]; - float transY = m[Matrix.MTRANS_Y]; - - float fixTransX = getFixTrans(transX, viewWidth, getImageWidth()); - float fixTransY = getFixTrans(transY, viewHeight, getImageHeight()); - - if (fixTransX != 0 || fixTransY != 0) { - matrix.postTranslate(fixTransX, fixTransY); - } - } - - /** - * When transitioning from zooming from focus to zoom from center (or vice versa) - * the image can become unaligned within the view. This is apparent when zooming - * quickly. When the content size is less than the view size, the content will often - * be centered incorrectly within the view. fixScaleTrans first calls fixTrans() and - * then makes sure the image is centered correctly within the view. - */ - private void fixScaleTrans() { - fixTrans(); - matrix.getValues(m); - if (getImageWidth() < viewWidth) { - m[Matrix.MTRANS_X] = (viewWidth - getImageWidth()) / 2; - } - - if (getImageHeight() < viewHeight) { - m[Matrix.MTRANS_Y] = (viewHeight - getImageHeight()) / 2; - } - matrix.setValues(m); - } - - private float getFixTrans(float trans, float viewSize, float contentSize) { - float minTrans, maxTrans; - - if (contentSize <= viewSize) { - minTrans = 0; - maxTrans = viewSize - contentSize; - - } else { - minTrans = viewSize - contentSize; - maxTrans = 0; - } - - if (trans < minTrans) - return -trans + minTrans; - if (trans > maxTrans) - return -trans + maxTrans; - return 0; - } - - private float getFixDragTrans(float delta, float viewSize, float contentSize) { - if (contentSize <= viewSize) { - return 0; - } - return delta; - } - - private float getImageWidth() { - return matchViewWidth * normalizedScale; - } - - private float getImageHeight() { - return matchViewHeight * normalizedScale; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - Drawable drawable = getDrawable(); - if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) { - setMeasuredDimension(0, 0); - return; - } - - int drawableWidth = drawable.getIntrinsicWidth(); - int drawableHeight = drawable.getIntrinsicHeight(); - int widthSize = MeasureSpec.getSize(widthMeasureSpec); - int widthMode = MeasureSpec.getMode(widthMeasureSpec); - int heightSize = MeasureSpec.getSize(heightMeasureSpec); - int heightMode = MeasureSpec.getMode(heightMeasureSpec); - viewWidth = setViewSize(widthMode, widthSize, drawableWidth); - viewHeight = setViewSize(heightMode, heightSize, drawableHeight); - - // - // Set view dimensions - // - setMeasuredDimension(viewWidth, viewHeight); - - // - // Fit content within view - // - fitImageToView(); - } - - /** - * If the normalizedScale is equal to 1, then the image is made to fit the screen. Otherwise, - * it is made to fit the screen according to the dimensions of the previous image matrix. This - * allows the image to maintain its zoom after rotation. - */ - private void fitImageToView() { - Drawable drawable = getDrawable(); - if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) { - return; - } - if (matrix == null || prevMatrix == null) { - return; - } - - int drawableWidth = drawable.getIntrinsicWidth(); - int drawableHeight = drawable.getIntrinsicHeight(); - - // - // Scale image for view - // - float scaleX = (float) viewWidth / drawableWidth; - float scaleY = (float) viewHeight / drawableHeight; - - switch (mScaleType) { - case CENTER: - scaleX = scaleY = 1; - break; - - case CENTER_CROP: - scaleX = scaleY = Math.max(scaleX, scaleY); - break; - - case CENTER_INSIDE: - scaleX = scaleY = Math.min(1, Math.min(scaleX, scaleY)); - - case FIT_CENTER: - scaleX = scaleY = Math.min(scaleX, scaleY); - break; - - case FIT_XY: - break; - - default: - // - // FIT_START and FIT_END not supported - // - throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END"); - - } - - // - // Center the image - // - float redundantXSpace = viewWidth - (scaleX * drawableWidth); - float redundantYSpace = viewHeight - (scaleY * drawableHeight); - matchViewWidth = viewWidth - redundantXSpace; - matchViewHeight = viewHeight - redundantYSpace; - if (!isZoomed() && !imageRenderedAtLeastOnce) { - // - // Stretch and center image to fit view - // - matrix.setScale(scaleX, scaleY); - matrix.postTranslate(redundantXSpace / 2, redundantYSpace / 2); - normalizedScale = 1; - - } else { - // - // These values should never be 0 or we will set viewWidth and viewHeight - // to NaN in translateMatrixAfterRotate. To avoid this, call savePreviousImageValues - // to set them equal to the current values. - // - if (prevMatchViewWidth == 0 || prevMatchViewHeight == 0) { - savePreviousImageValues(); - } - - prevMatrix.getValues(m); - - // - // Rescale Matrix after rotation - // - m[Matrix.MSCALE_X] = matchViewWidth / drawableWidth * normalizedScale; - m[Matrix.MSCALE_Y] = matchViewHeight / drawableHeight * normalizedScale; - - // - // TransX and TransY from previous matrix - // - float transX = m[Matrix.MTRANS_X]; - float transY = m[Matrix.MTRANS_Y]; - - // - // Width - // - float prevActualWidth = prevMatchViewWidth * normalizedScale; - float actualWidth = getImageWidth(); - translateMatrixAfterRotate(Matrix.MTRANS_X, transX, prevActualWidth, actualWidth, prevViewWidth, viewWidth, drawableWidth); - - // - // Height - // - float prevActualHeight = prevMatchViewHeight * normalizedScale; - float actualHeight = getImageHeight(); - translateMatrixAfterRotate(Matrix.MTRANS_Y, transY, prevActualHeight, actualHeight, prevViewHeight, viewHeight, drawableHeight); - - // - // Set the matrix to the adjusted scale and translate values. - // - matrix.setValues(m); - } - fixTrans(); - setImageMatrix(matrix); - } - - /** - * Set view dimensions based on layout params - * - * @param mode - * @param size - * @param drawableWidth - * @return - */ - private int setViewSize(int mode, int size, int drawableWidth) { - int viewSize; - switch (mode) { - case MeasureSpec.EXACTLY: - viewSize = size; - break; - - case MeasureSpec.AT_MOST: - viewSize = Math.min(drawableWidth, size); - break; - - case MeasureSpec.UNSPECIFIED: - viewSize = drawableWidth; - break; - - default: - viewSize = size; - break; - } - return viewSize; - } - - /** - * After rotating, the matrix needs to be translated. This function finds the area of image - * which was previously centered and adjusts translations so that is again the center, post-rotation. - * - * @param axis Matrix.MTRANS_X or Matrix.MTRANS_Y - * @param trans the value of trans in that axis before the rotation - * @param prevImageSize the width/height of the image before the rotation - * @param imageSize width/height of the image after rotation - * @param prevViewSize width/height of view before rotation - * @param viewSize width/height of view after rotation - * @param drawableSize width/height of drawable - */ - private void translateMatrixAfterRotate(int axis, float trans, float prevImageSize, float imageSize, int prevViewSize, int viewSize, int drawableSize) { - if (imageSize < viewSize) { - // - // The width/height of image is less than the view's width/height. Center it. - // - m[axis] = (viewSize - (drawableSize * m[Matrix.MSCALE_X])) * 0.5f; - - } else if (trans > 0) { - // - // The image is larger than the view, but was not before rotation. Center it. - // - m[axis] = -((imageSize - viewSize) * 0.5f); - - } else { - // - // Find the area of the image which was previously centered in the view. Determine its distance - // from the left/top side of the view as a fraction of the entire image's width/height. Use that percentage - // to calculate the trans in the new view width/height. - // - float percentage = (Math.abs(trans) + (0.5f * prevViewSize)) / prevImageSize; - m[axis] = -((percentage * imageSize) - (viewSize * 0.5f)); - } - } - - private void setState(State state) { - this.state = state; - } - - public boolean canScrollHorizontallyFroyo(int direction) { - return canScrollHorizontally(direction); - } - - @Override - public boolean canScrollHorizontally(int direction) { - matrix.getValues(m); - float x = m[Matrix.MTRANS_X]; - - if (getImageWidth() < viewWidth) { - return false; - - } else if (x >= -1 && direction < 0) { - return false; - - } else if (Math.abs(x) + viewWidth + 1 >= getImageWidth() && direction > 0) { - return false; - } - - return true; - } - - /** - * Gesture Listener detects a single click or long click and passes that on - * to the view's listener. - * @author Ortiz - * - */ - private class GestureListener extends GestureDetector.SimpleOnGestureListener { - - @Override - public boolean onSingleTapConfirmed(MotionEvent e) - { - if(doubleTapListener != null) { - return doubleTapListener.onSingleTapConfirmed(e); - } - return performClick(); - } - - @Override - public void onLongPress(MotionEvent e) - { - performLongClick(); - } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) - { - if (fling != null) { - // - // If a previous fling is still active, it should be cancelled so that two flings - // are not run simultaenously. - // - fling.cancelFling(); - } - fling = new Fling((int) velocityX, (int) velocityY); - compatPostOnAnimation(fling); - return super.onFling(e1, e2, velocityX, velocityY); - } - - @Override - public boolean onDoubleTap(MotionEvent e) { - boolean consumed = false; - if(doubleTapListener != null) { - consumed = doubleTapListener.onDoubleTap(e); - } - if (state == State.NONE) { - float targetZoom = (normalizedScale == minScale) ? maxScale : minScale; - DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, e.getX(), e.getY(), false); - compatPostOnAnimation(doubleTap); - consumed = true; - } - return consumed; - } - - @Override - public boolean onDoubleTapEvent(MotionEvent e) { - if(doubleTapListener != null) { - return doubleTapListener.onDoubleTapEvent(e); - } - return false; - } - } - - public interface OnTouchImageViewListener { - public void onMove(); - } - - /** - * Responsible for all touch events. Handles the heavy lifting of drag and also sends - * touch events to Scale Detector and Gesture Detector. - * @author Ortiz - * - */ - private class PrivateOnTouchListener implements OnTouchListener { - - // - // Remember last point position for dragging - // - private PointF last = new PointF(); - - @Override - public boolean onTouch(View v, MotionEvent event) { - mScaleDetector.onTouchEvent(event); - mGestureDetector.onTouchEvent(event); - PointF curr = new PointF(event.getX(), event.getY()); - - if (state == State.NONE || state == State.DRAG || state == State.FLING) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - last.set(curr); - if (fling != null) - fling.cancelFling(); - setState(State.DRAG); - break; - - case MotionEvent.ACTION_MOVE: - if (state == State.DRAG) { - float deltaX = curr.x - last.x; - float deltaY = curr.y - last.y; - float fixTransX = getFixDragTrans(deltaX, viewWidth, getImageWidth()); - float fixTransY = getFixDragTrans(deltaY, viewHeight, getImageHeight()); - matrix.postTranslate(fixTransX, fixTransY); - fixTrans(); - last.set(curr.x, curr.y); - } - break; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - setState(State.NONE); - break; - } - } - - setImageMatrix(matrix); - - // - // User-defined OnTouchListener - // - if(userTouchListener != null) { - userTouchListener.onTouch(v, event); - } - - // - // OnTouchImageViewListener is set: TouchImageView dragged by user. - // - if (touchImageViewListener != null) { - touchImageViewListener.onMove(); - } - - // - // indicate event was handled - // - return true; - } - } - - /** - * ScaleListener detects user two finger scaling and scales image. - * @author Ortiz - * - */ - private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { - @Override - public boolean onScaleBegin(ScaleGestureDetector detector) { - setState(State.ZOOM); - return true; - } - - @Override - public boolean onScale(ScaleGestureDetector detector) { - scaleImage(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY(), true); - - // - // OnTouchImageViewListener is set: TouchImageView pinch zoomed by user. - // - if (touchImageViewListener != null) { - touchImageViewListener.onMove(); - } - return true; - } - - @Override - public void onScaleEnd(ScaleGestureDetector detector) { - super.onScaleEnd(detector); - setState(State.NONE); - boolean animateToZoomBoundary = false; - float targetZoom = normalizedScale; - if (normalizedScale > maxScale) { - targetZoom = maxScale; - animateToZoomBoundary = true; - - } else if (normalizedScale < minScale) { - targetZoom = minScale; - animateToZoomBoundary = true; - } - - if (animateToZoomBoundary) { - DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, viewWidth / 2, viewHeight / 2, true); - compatPostOnAnimation(doubleTap); - } - } - } - - private void scaleImage(double deltaScale, float focusX, float focusY, boolean stretchImageToSuper) { - - float lowerScale, upperScale; - if (stretchImageToSuper) { - lowerScale = superMinScale; - upperScale = superMaxScale; - - } else { - lowerScale = minScale; - upperScale = maxScale; - } - - float origScale = normalizedScale; - normalizedScale *= deltaScale; - if (normalizedScale > upperScale) { - normalizedScale = upperScale; - deltaScale = upperScale / origScale; - } else if (normalizedScale < lowerScale) { - normalizedScale = lowerScale; - deltaScale = lowerScale / origScale; - } - - matrix.postScale((float) deltaScale, (float) deltaScale, focusX, focusY); - fixScaleTrans(); - } - - /** - * DoubleTapZoom calls a series of runnables which apply - * an animated zoom in/out graphic to the image. - * @author Ortiz - * - */ - private class DoubleTapZoom implements Runnable { - - private long startTime; - private static final float ZOOM_TIME = 500; - private float startZoom, targetZoom; - private float bitmapX, bitmapY; - private boolean stretchImageToSuper; - private AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator(); - private PointF startTouch; - private PointF endTouch; - - DoubleTapZoom(float targetZoom, float focusX, float focusY, boolean stretchImageToSuper) { - setState(State.ANIMATE_ZOOM); - startTime = System.currentTimeMillis(); - this.startZoom = normalizedScale; - this.targetZoom = targetZoom; - this.stretchImageToSuper = stretchImageToSuper; - PointF bitmapPoint = transformCoordTouchToBitmap(focusX, focusY, false); - this.bitmapX = bitmapPoint.x; - this.bitmapY = bitmapPoint.y; - - // - // Used for translating image during scaling - // - startTouch = transformCoordBitmapToTouch(bitmapX, bitmapY); - endTouch = new PointF(viewWidth / 2, viewHeight / 2); - } - - @Override - public void run() { - float t = interpolate(); - double deltaScale = calculateDeltaScale(t); - scaleImage(deltaScale, bitmapX, bitmapY, stretchImageToSuper); - translateImageToCenterTouchPosition(t); - fixScaleTrans(); - setImageMatrix(matrix); - - // - // OnTouchImageViewListener is set: double tap runnable updates listener - // with every frame. - // - if (touchImageViewListener != null) { - touchImageViewListener.onMove(); - } - - if (t < 1f) { - // - // We haven't finished zooming - // - compatPostOnAnimation(this); - - } else { - // - // Finished zooming - // - setState(State.NONE); - } - } - - /** - * Interpolate between where the image should start and end in order to translate - * the image so that the point that is touched is what ends up centered at the end - * of the zoom. - * @param t - */ - private void translateImageToCenterTouchPosition(float t) { - float targetX = startTouch.x + t * (endTouch.x - startTouch.x); - float targetY = startTouch.y + t * (endTouch.y - startTouch.y); - PointF curr = transformCoordBitmapToTouch(bitmapX, bitmapY); - matrix.postTranslate(targetX - curr.x, targetY - curr.y); - } - - /** - * Use interpolator to get t - * @return - */ - private float interpolate() { - long currTime = System.currentTimeMillis(); - float elapsed = (currTime - startTime) / ZOOM_TIME; - elapsed = Math.min(1f, elapsed); - return interpolator.getInterpolation(elapsed); - } - - /** - * Interpolate the current targeted zoom and get the delta - * from the current zoom. - * @param t - * @return - */ - private double calculateDeltaScale(float t) { - double zoom = startZoom + t * (targetZoom - startZoom); - return zoom / normalizedScale; - } - } - - /** - * This function will transform the coordinates in the touch event to the coordinate - * system of the drawable that the imageview contain - * @param x x-coordinate of touch event - * @param y y-coordinate of touch event - * @param clipToBitmap Touch event may occur within view, but outside image content. True, to clip return value - * to the bounds of the bitmap size. - * @return Coordinates of the point touched, in the coordinate system of the original drawable. - */ - private PointF transformCoordTouchToBitmap(float x, float y, boolean clipToBitmap) { - matrix.getValues(m); - float origW = getDrawable().getIntrinsicWidth(); - float origH = getDrawable().getIntrinsicHeight(); - float transX = m[Matrix.MTRANS_X]; - float transY = m[Matrix.MTRANS_Y]; - float finalX = ((x - transX) * origW) / getImageWidth(); - float finalY = ((y - transY) * origH) / getImageHeight(); - - if (clipToBitmap) { - finalX = Math.min(Math.max(finalX, 0), origW); - finalY = Math.min(Math.max(finalY, 0), origH); - } - - return new PointF(finalX , finalY); - } - - /** - * Inverse of transformCoordTouchToBitmap. This function will transform the coordinates in the - * drawable's coordinate system to the view's coordinate system. - * @param bx x-coordinate in original bitmap coordinate system - * @param by y-coordinate in original bitmap coordinate system - * @return Coordinates of the point in the view's coordinate system. - */ - private PointF transformCoordBitmapToTouch(float bx, float by) { - matrix.getValues(m); - float origW = getDrawable().getIntrinsicWidth(); - float origH = getDrawable().getIntrinsicHeight(); - float px = bx / origW; - float py = by / origH; - float finalX = m[Matrix.MTRANS_X] + getImageWidth() * px; - float finalY = m[Matrix.MTRANS_Y] + getImageHeight() * py; - return new PointF(finalX , finalY); - } - - /** - * Fling launches sequential runnables which apply - * the fling graphic to the image. The values for the translation - * are interpolated by the Scroller. - * @author Ortiz - * - */ - private class Fling implements Runnable { - - CompatScroller scroller; - int currX, currY; - - Fling(int velocityX, int velocityY) { - setState(State.FLING); - scroller = new CompatScroller(context); - matrix.getValues(m); - - int startX = (int) m[Matrix.MTRANS_X]; - int startY = (int) m[Matrix.MTRANS_Y]; - int minX, maxX, minY, maxY; - - if (getImageWidth() > viewWidth) { - minX = viewWidth - (int) getImageWidth(); - maxX = 0; - - } else { - minX = maxX = startX; - } - - if (getImageHeight() > viewHeight) { - minY = viewHeight - (int) getImageHeight(); - maxY = 0; - - } else { - minY = maxY = startY; - } - - scroller.fling(startX, startY, (int) velocityX, (int) velocityY, minX, - maxX, minY, maxY); - currX = startX; - currY = startY; - } - - public void cancelFling() { - if (scroller != null) { - setState(State.NONE); - scroller.forceFinished(true); - } - } - - @Override - public void run() { - - // - // OnTouchImageViewListener is set: TouchImageView listener has been flung by user. - // Listener runnable updated with each frame of fling animation. - // - if (touchImageViewListener != null) { - touchImageViewListener.onMove(); - } - - if (scroller.isFinished()) { - scroller = null; - return; - } - - if (scroller.computeScrollOffset()) { - int newX = scroller.getCurrX(); - int newY = scroller.getCurrY(); - int transX = newX - currX; - int transY = newY - currY; - currX = newX; - currY = newY; - matrix.postTranslate(transX, transY); - fixTrans(); - setImageMatrix(matrix); - compatPostOnAnimation(this); - } - } - } - - @TargetApi(Build.VERSION_CODES.GINGERBREAD) - private class CompatScroller { - Scroller scroller; - OverScroller overScroller; - boolean isPreGingerbread; - - public CompatScroller(Context context) { - if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD) { - isPreGingerbread = true; - scroller = new Scroller(context); - - } else { - isPreGingerbread = false; - overScroller = new OverScroller(context); - } - } - - public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) { - if (isPreGingerbread) { - scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); - } else { - overScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); - } - } - - public void forceFinished(boolean finished) { - if (isPreGingerbread) { - scroller.forceFinished(finished); - } else { - overScroller.forceFinished(finished); - } - } - - public boolean isFinished() { - if (isPreGingerbread) { - return scroller.isFinished(); - } else { - return overScroller.isFinished(); - } - } - - public boolean computeScrollOffset() { - if (isPreGingerbread) { - return scroller.computeScrollOffset(); - } else { - overScroller.computeScrollOffset(); - return overScroller.computeScrollOffset(); - } - } - - public int getCurrX() { - if (isPreGingerbread) { - return scroller.getCurrX(); - } else { - return overScroller.getCurrX(); - } - } - - public int getCurrY() { - if (isPreGingerbread) { - return scroller.getCurrY(); - } else { - return overScroller.getCurrY(); - } - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - private void compatPostOnAnimation(Runnable runnable) { - if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { - postOnAnimation(runnable); - - } else { - postDelayed(runnable, 1000/60); - } - } - - private class ZoomVariables { - public float scale; - public float focusX; - public float focusY; - public ScaleType scaleType; - - public ZoomVariables(float scale, float focusX, float focusY, ScaleType scaleType) { - this.scale = scale; - this.focusX = focusX; - this.focusY = focusY; - this.scaleType = scaleType; - } - } - - private void printMatrixInfo() { - float[] n = new float[9]; - matrix.getValues(n); - Log.d(DEBUG, "Scale: " + n[Matrix.MSCALE_X] + " TransX: " + n[Matrix.MTRANS_X] + " TransY: " + n[Matrix.MTRANS_Y]); - } -} \ No newline at end of file diff --git a/src/com/owncloud/android/utils/UriUtils.java b/src/com/owncloud/android/utils/UriUtils.java index e66d2c9f..8070e360 100644 --- a/src/com/owncloud/android/utils/UriUtils.java +++ b/src/com/owncloud/android/utils/UriUtils.java @@ -1,5 +1,7 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. +/** + * 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, diff --git a/src/com/owncloud/android/widgets/ActionEditText.java b/src/com/owncloud/android/widgets/ActionEditText.java index 1077d154..cb0dac4f 100644 --- a/src/com/owncloud/android/widgets/ActionEditText.java +++ b/src/com/owncloud/android/widgets/ActionEditText.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, diff --git a/src/third_parties/in/srain/cube/GridViewWithHeaderAndFooter.java b/src/third_parties/in/srain/cube/GridViewWithHeaderAndFooter.java new file mode 100644 index 00000000..508380a6 --- /dev/null +++ b/src/third_parties/in/srain/cube/GridViewWithHeaderAndFooter.java @@ -0,0 +1,841 @@ + +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package third_parties.in.srain.cube; + + +import android.annotation.TargetApi; +import android.content.Context; +import android.database.DataSetObservable; +import android.database.DataSetObserver; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.FrameLayout; +import android.widget.GridView; +import android.widget.ListAdapter; +import android.widget.WrapperListAdapter; + +import java.lang.reflect.Field; +import java.util.ArrayList; + +/** + * A {@link android.widget.GridView} that supports adding header rows in a + * very similar way to {@link android.widget.ListView}. + * See {@link GridViewWithHeaderAndFooter#addHeaderView(View, Object, boolean)} + * See {@link GridViewWithHeaderAndFooter#addFooterView(View, Object, boolean)} + */ +public class GridViewWithHeaderAndFooter extends GridView { + + public static boolean DEBUG = false; + + /** + * A class that represents a fixed view in a list, for example a header at the top + * or a footer at the bottom. + */ + private static class FixedViewInfo { + /** + * The view to add to the grid + */ + public View view; + public ViewGroup viewContainer; + /** + * The data backing the view. This is returned from {@link android.widget.ListAdapter#getItem(int)}. + */ + public Object data; + /** + * true if the fixed view should be selectable in the grid + */ + public boolean isSelectable; + } + + private int mNumColumns = AUTO_FIT; + private View mViewForMeasureRowHeight = null; + private int mRowHeight = -1; + private static final String LOG_TAG = "grid-view-with-header-and-footer"; + + private ArrayList mHeaderViewInfos = new ArrayList(); + private ArrayList mFooterViewInfos = new ArrayList(); + + private void initHeaderGridView() { + } + + public GridViewWithHeaderAndFooter(Context context) { + super(context); + initHeaderGridView(); + } + + public GridViewWithHeaderAndFooter(Context context, AttributeSet attrs) { + super(context, attrs); + initHeaderGridView(); + } + + public GridViewWithHeaderAndFooter(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initHeaderGridView(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + ListAdapter adapter = getAdapter(); + if (adapter != null && adapter instanceof HeaderViewGridAdapter) { + ((HeaderViewGridAdapter) adapter).setNumColumns(getNumColumnsCompatible()); + ((HeaderViewGridAdapter) adapter).setRowHeight(getRowHeight()); + } + } + + @Override + public void setClipChildren(boolean clipChildren) { + // Ignore, since the header rows depend on not being clipped + } + + /** + * Do not call this method unless you know how it works. + * + * @param clipChildren + */ + public void setClipChildrenSupper(boolean clipChildren) { + super.setClipChildren(false); + } + + /** + * Add a fixed view to appear at the top of the grid. If addHeaderView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + *

+ * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap + * the supplied cursor with one that will also account for header views. + * + * @param v The view to add. + */ + public void addHeaderView(View v) { + addHeaderView(v, null, true); + } + + /** + * Add a fixed view to appear at the top of the grid. If addHeaderView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + *

+ * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap + * the supplied cursor with one that will also account for header views. + * + * @param v The view to add. + * @param data Data to associate with this view + * @param isSelectable whether the item is selectable + */ + public void addHeaderView(View v, Object data, boolean isSelectable) { + ListAdapter adapter = getAdapter(); + if (adapter != null && !(adapter instanceof HeaderViewGridAdapter)) { + throw new IllegalStateException( + "Cannot add header view to grid -- setAdapter has already been called."); + } + + ViewGroup.LayoutParams lyp = v.getLayoutParams(); + + FixedViewInfo info = new FixedViewInfo(); + FrameLayout fl = new FullWidthFixedViewLayout(getContext()); + + if (lyp != null) { + v.setLayoutParams(new FrameLayout.LayoutParams(lyp.width, lyp.height)); + fl.setLayoutParams(new LayoutParams(lyp.width, lyp.height)); + } + fl.addView(v); + info.view = v; + info.viewContainer = fl; + info.data = data; + info.isSelectable = isSelectable; + mHeaderViewInfos.add(info); + // in the case of re-adding a header view, or adding one later on, + // we need to notify the observer + if (adapter != null) { + ((HeaderViewGridAdapter) adapter).notifyDataSetChanged(); + } + } + + public void addFooterView(View v) { + addFooterView(v, null, true); + } + + public void addFooterView(View v, Object data, boolean isSelectable) { + ListAdapter mAdapter = getAdapter(); + if (mAdapter != null && !(mAdapter instanceof HeaderViewGridAdapter)) { + throw new IllegalStateException( + "Cannot add header view to grid -- setAdapter has already been called."); + } + + ViewGroup.LayoutParams lyp = v.getLayoutParams(); + + FixedViewInfo info = new FixedViewInfo(); + FrameLayout fl = new FullWidthFixedViewLayout(getContext()); + + if (lyp != null) { + v.setLayoutParams(new FrameLayout.LayoutParams(lyp.width, lyp.height)); + fl.setLayoutParams(new LayoutParams(lyp.width, lyp.height)); + } + fl.addView(v); + info.view = v; + info.viewContainer = fl; + info.data = data; + info.isSelectable = isSelectable; + mFooterViewInfos.add(info); + + if (mAdapter != null) { + ((HeaderViewGridAdapter) mAdapter).notifyDataSetChanged(); + } + } + + public int getHeaderViewCount() { + return mHeaderViewInfos.size(); + } + + public int getFooterViewCount() { + return mFooterViewInfos.size(); + } + + /** + * Removes a previously-added header view. + * + * @param v The view to remove + * @return true if the view was removed, false if the view was not a header + * view + */ + public boolean removeHeaderView(View v) { + if (mHeaderViewInfos.size() > 0) { + boolean result = false; + ListAdapter adapter = getAdapter(); + if (adapter != null && ((HeaderViewGridAdapter) adapter).removeHeader(v)) { + result = true; + } + removeFixedViewInfo(v, mHeaderViewInfos); + return result; + } + return false; + } + + /** + * Removes a previously-added footer view. + * + * @param v The view to remove + * @return true if the view was removed, false if the view was not a header + * view + */ + public boolean removeFooterView(View v) { + if (mFooterViewInfos.size() > 0) { + boolean result = false; + ListAdapter adapter = getAdapter(); + if (adapter != null && ((HeaderViewGridAdapter) adapter).removeFooter(v)) { + result = true; + } + removeFixedViewInfo(v, mFooterViewInfos); + return result; + } + return false; + } + + private void removeFixedViewInfo(View v, ArrayList where) { + int len = where.size(); + for (int i = 0; i < len; ++i) { + FixedViewInfo info = where.get(i); + if (info.view == v) { + where.remove(i); + break; + } + } + } + + @TargetApi(11) + private int getNumColumnsCompatible() { + if (Build.VERSION.SDK_INT >= 11) { + return super.getNumColumns(); + } else { + try { + Field numColumns = getClass().getSuperclass().getDeclaredField("mNumColumns"); + numColumns.setAccessible(true); + return numColumns.getInt(this); + } catch (Exception e) { + if (mNumColumns != -1) { + return mNumColumns; + } + throw new RuntimeException("Can not determine the mNumColumns for this API platform, please call setNumColumns to set it."); + } + } + } + + @TargetApi(16) + private int getColumnWidthCompatible() { + if (Build.VERSION.SDK_INT >= 16) { + return super.getColumnWidth(); + } else { + try { + Field numColumns = getClass().getSuperclass().getDeclaredField("mColumnWidth"); + numColumns.setAccessible(true); + return numColumns.getInt(this); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mViewForMeasureRowHeight = null; + } + + public void invalidateRowHeight() { + mRowHeight = -1; + } + + public int getRowHeight() { + if (mRowHeight > 0) { + return mRowHeight; + } + ListAdapter adapter = getAdapter(); + int numColumns = getNumColumnsCompatible(); + + // adapter has not been set or has no views in it; + if (adapter == null || adapter.getCount() <= numColumns * (mHeaderViewInfos.size() + mFooterViewInfos.size())) { + return -1; + } + int mColumnWidth = getColumnWidthCompatible(); + View view = getAdapter().getView(numColumns * mHeaderViewInfos.size(), mViewForMeasureRowHeight, this); + LayoutParams p = (LayoutParams) view.getLayoutParams(); + if (p == null) { + p = new LayoutParams(-1, -2, 0); + view.setLayoutParams(p); + } + int childHeightSpec = getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); + int childWidthSpec = getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); + view.measure(childWidthSpec, childHeightSpec); + mViewForMeasureRowHeight = view; + mRowHeight = view.getMeasuredHeight(); + return mRowHeight; + } + + @TargetApi(11) + public void tryToScrollToBottomSmoothly() { + int lastPos = getAdapter().getCount() - 1; + if (Build.VERSION.SDK_INT >= 11) { + smoothScrollToPositionFromTop(lastPos, 0); + } else { + setSelection(lastPos); + } + } + + @TargetApi(11) + public void tryToScrollToBottomSmoothly(int duration) { + int lastPos = getAdapter().getCount() - 1; + if (Build.VERSION.SDK_INT >= 11) { + smoothScrollToPositionFromTop(lastPos, 0, duration); + } else { + setSelection(lastPos); + } + } + + @Override + public void setAdapter(ListAdapter adapter) { + if (mHeaderViewInfos.size() > 0 || mFooterViewInfos.size() > 0) { + HeaderViewGridAdapter headerViewGridAdapter = new HeaderViewGridAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); + int numColumns = getNumColumnsCompatible(); + if (numColumns > 1) { + headerViewGridAdapter.setNumColumns(numColumns); + } + headerViewGridAdapter.setRowHeight(getRowHeight()); + super.setAdapter(headerViewGridAdapter); + } else { + super.setAdapter(adapter); + } + } + + /** + * full width + */ + private class FullWidthFixedViewLayout extends FrameLayout { + + public FullWidthFixedViewLayout(Context context) { + super(context); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int realLeft = GridViewWithHeaderAndFooter.this.getPaddingLeft() + getPaddingLeft(); + // Try to make where it should be, from left, full width + if (realLeft != left) { + offsetLeftAndRight(realLeft - left); + } + super.onLayout(changed, left, top, right, bottom); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int targetWidth = GridViewWithHeaderAndFooter.this.getMeasuredWidth() + - GridViewWithHeaderAndFooter.this.getPaddingLeft() + - GridViewWithHeaderAndFooter.this.getPaddingRight(); + widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth, + MeasureSpec.getMode(widthMeasureSpec)); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + @Override + public void setNumColumns(int numColumns) { + super.setNumColumns(numColumns); + mNumColumns = numColumns; + ListAdapter adapter = getAdapter(); + if (adapter != null && adapter instanceof HeaderViewGridAdapter) { + ((HeaderViewGridAdapter) adapter).setNumColumns(numColumns); + } + } + + /** + * ListAdapter used when a HeaderGridView has header views. This ListAdapter + * wraps another one and also keeps track of the header views and their + * associated data objects. + *

This is intended as a base class; you will probably not need to + * use this class directly in your own code. + */ + private static class HeaderViewGridAdapter implements WrapperListAdapter, Filterable { + // This is used to notify the container of updates relating to number of columns + // or headers changing, which changes the number of placeholders needed + private final DataSetObservable mDataSetObservable = new DataSetObservable(); + private final ListAdapter mAdapter; + static final ArrayList EMPTY_INFO_LIST = + new ArrayList(); + + // This ArrayList is assumed to NOT be null. + ArrayList mHeaderViewInfos; + ArrayList mFooterViewInfos; + private int mNumColumns = 1; + private int mRowHeight = -1; + boolean mAreAllFixedViewsSelectable; + private final boolean mIsFilterable; + private boolean mCachePlaceHoldView = true; + // From Recycle Bin or calling getView, this a question... + private boolean mCacheFirstHeaderView = false; + + public HeaderViewGridAdapter(ArrayList headerViewInfos, ArrayList footViewInfos, ListAdapter adapter) { + mAdapter = adapter; + mIsFilterable = adapter instanceof Filterable; + if (headerViewInfos == null) { + mHeaderViewInfos = EMPTY_INFO_LIST; + } else { + mHeaderViewInfos = headerViewInfos; + } + + if (footViewInfos == null) { + mFooterViewInfos = EMPTY_INFO_LIST; + } else { + mFooterViewInfos = footViewInfos; + } + mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos) + && areAllListInfosSelectable(mFooterViewInfos); + } + + public void setNumColumns(int numColumns) { + if (numColumns < 1) { + return; + } + if (mNumColumns != numColumns) { + mNumColumns = numColumns; + notifyDataSetChanged(); + } + } + + public void setRowHeight(int height) { + mRowHeight = height; + } + + public int getHeadersCount() { + return mHeaderViewInfos.size(); + } + + public int getFootersCount() { + return mFooterViewInfos.size(); + } + + @Override + public boolean isEmpty() { + return (mAdapter == null || mAdapter.isEmpty()) && getHeadersCount() == 0 && getFootersCount() == 0; + } + + private boolean areAllListInfosSelectable(ArrayList infos) { + if (infos != null) { + for (FixedViewInfo info : infos) { + if (!info.isSelectable) { + return false; + } + } + } + return true; + } + + public boolean removeHeader(View v) { + for (int i = 0; i < mHeaderViewInfos.size(); i++) { + FixedViewInfo info = mHeaderViewInfos.get(i); + if (info.view == v) { + mHeaderViewInfos.remove(i); + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) && areAllListInfosSelectable(mFooterViewInfos); + mDataSetObservable.notifyChanged(); + return true; + } + } + return false; + } + + public boolean removeFooter(View v) { + for (int i = 0; i < mFooterViewInfos.size(); i++) { + FixedViewInfo info = mFooterViewInfos.get(i); + if (info.view == v) { + mFooterViewInfos.remove(i); + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) && areAllListInfosSelectable(mFooterViewInfos); + mDataSetObservable.notifyChanged(); + return true; + } + } + return false; + } + + @Override + public int getCount() { + if (mAdapter != null) { + return (getFootersCount() + getHeadersCount()) * mNumColumns + getAdapterAndPlaceHolderCount(); + } else { + return (getFootersCount() + getHeadersCount()) * mNumColumns; + } + } + + @Override + public boolean areAllItemsEnabled() { + if (mAdapter != null) { + return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled(); + } else { + return true; + } + } + + private int getAdapterAndPlaceHolderCount() { + final int adapterCount = (int) (Math.ceil(1f * mAdapter.getCount() / mNumColumns) * mNumColumns); + return adapterCount; + } + + @Override + public boolean isEnabled(int position) { + // Header (negative positions will throw an IndexOutOfBoundsException) + int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + if (position < numHeadersAndPlaceholders) { + return position % mNumColumns == 0 + && mHeaderViewInfos.get(position / mNumColumns).isSelectable; + } + + // Adapter + final int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = getAdapterAndPlaceHolderCount(); + if (adjPosition < adapterCount) { + return adjPosition < mAdapter.getCount() && mAdapter.isEnabled(adjPosition); + } + } + + // Footer (off-limits positions will throw an IndexOutOfBoundsException) + final int footerPosition = adjPosition - adapterCount; + return footerPosition % mNumColumns == 0 + && mFooterViewInfos.get(footerPosition / mNumColumns).isSelectable; + } + + @Override + public Object getItem(int position) { + // Header (negative positions will throw an ArrayIndexOutOfBoundsException) + int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + if (position < numHeadersAndPlaceholders) { + if (position % mNumColumns == 0) { + return mHeaderViewInfos.get(position / mNumColumns).data; + } + return null; + } + + // Adapter + final int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = getAdapterAndPlaceHolderCount(); + if (adjPosition < adapterCount) { + if (adjPosition < mAdapter.getCount()) { + return mAdapter.getItem(adjPosition); + } else { + return null; + } + } + } + + // Footer (off-limits positions will throw an IndexOutOfBoundsException) + final int footerPosition = adjPosition - adapterCount; + if (footerPosition % mNumColumns == 0) { + return mFooterViewInfos.get(footerPosition).data; + } else { + return null; + } + } + + @Override + public long getItemId(int position) { + int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + if (mAdapter != null && position >= numHeadersAndPlaceholders) { + int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getItemId(adjPosition); + } + } + return -1; + } + + @Override + public boolean hasStableIds() { + if (mAdapter != null) { + return mAdapter.hasStableIds(); + } + return false; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (DEBUG) { + Log.d(LOG_TAG, String.format("getView: %s, reused: %s", position, convertView == null)); + } + // Header (negative positions will throw an ArrayIndexOutOfBoundsException) + int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + if (position < numHeadersAndPlaceholders) { + View headerViewContainer = mHeaderViewInfos + .get(position / mNumColumns).viewContainer; + if (position % mNumColumns == 0) { + return headerViewContainer; + } else { + if (convertView == null) { + convertView = new View(parent.getContext()); + } + // We need to do this because GridView uses the height of the last item + // in a row to determine the height for the entire row. + convertView.setVisibility(View.INVISIBLE); + convertView.setMinimumHeight(headerViewContainer.getHeight()); + return convertView; + } + } + // Adapter + final int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = getAdapterAndPlaceHolderCount(); + if (adjPosition < adapterCount) { + if (adjPosition < mAdapter.getCount()) { + View view = mAdapter.getView(adjPosition, convertView, parent); + return view; + } else { + if (convertView == null) { + convertView = new View(parent.getContext()); + } + convertView.setVisibility(View.INVISIBLE); + convertView.setMinimumHeight(mRowHeight); + return convertView; + } + } + } + // Footer + final int footerPosition = adjPosition - adapterCount; + if (footerPosition < getCount()) { + View footViewContainer = mFooterViewInfos + .get(footerPosition / mNumColumns).viewContainer; + if (position % mNumColumns == 0) { + return footViewContainer; + } else { + if (convertView == null) { + convertView = new View(parent.getContext()); + } + // We need to do this because GridView uses the height of the last item + // in a row to determine the height for the entire row. + convertView.setVisibility(View.INVISIBLE); + convertView.setMinimumHeight(footViewContainer.getHeight()); + return convertView; + } + } + throw new ArrayIndexOutOfBoundsException(position); + } + + @Override + public int getItemViewType(int position) { + + final int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + final int adapterViewTypeStart = mAdapter == null ? 0 : mAdapter.getViewTypeCount() - 1; + int type = AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER; + if (mCachePlaceHoldView) { + // Header + if (position < numHeadersAndPlaceholders) { + if (position == 0) { + if (mCacheFirstHeaderView) { + type = adapterViewTypeStart + mHeaderViewInfos.size() + mFooterViewInfos.size() + 1 + 1; + } + } + if (position % mNumColumns != 0) { + type = adapterViewTypeStart + (position / mNumColumns + 1); + } + } + } + + // Adapter + final int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = getAdapterAndPlaceHolderCount(); + if (adjPosition >= 0 && adjPosition < adapterCount) { + if (adjPosition < mAdapter.getCount()) { + type = mAdapter.getItemViewType(adjPosition); + } else { + if (mCachePlaceHoldView) { + type = adapterViewTypeStart + mHeaderViewInfos.size() + 1; + } + } + } + } + + if (mCachePlaceHoldView) { + // Footer + final int footerPosition = adjPosition - adapterCount; + if (footerPosition >= 0 && footerPosition < getCount() && (footerPosition % mNumColumns) != 0) { + type = adapterViewTypeStart + mHeaderViewInfos.size() + 1 + (footerPosition / mNumColumns + 1); + } + } + if (DEBUG) { + Log.d(LOG_TAG, String.format("getItemViewType: pos: %s, result: %s", position, type, mCachePlaceHoldView, mCacheFirstHeaderView)); + } + return type; + } + + /** + * content view, content view holder, header[0], header and footer placeholder(s) + * + * @return + */ + @Override + public int getViewTypeCount() { + int count = mAdapter == null ? 1 : mAdapter.getViewTypeCount(); + if (mCachePlaceHoldView) { + int offset = mHeaderViewInfos.size() + 1 + mFooterViewInfos.size(); + if (mCacheFirstHeaderView) { + offset += 1; + } + count += offset; + } + if (DEBUG) { + Log.d(LOG_TAG, String.format("getViewTypeCount: %s", count)); + } + return count; + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + mDataSetObservable.registerObserver(observer); + if (mAdapter != null) { + mAdapter.registerDataSetObserver(observer); + } + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + mDataSetObservable.unregisterObserver(observer); + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(observer); + } + } + + @Override + public Filter getFilter() { + if (mIsFilterable) { + return ((Filterable) mAdapter).getFilter(); + } + return null; + } + + @Override + public ListAdapter getWrappedAdapter() { + return mAdapter; + } + + public void notifyDataSetChanged() { + mDataSetObservable.notifyChanged(); + } + } + + + /** + * Sets the selected item and positions the selection y pixels from the top edge of the ListView. + * (If in touch mode, the item will not be selected but it will still be positioned appropriately.) + * + * @param position Index (starting at 0) of the data item to be selected. + * @param y The distance from the top edge of the ListView (plus padding) + * that the item will be positioned. + * + * @see Original code + */ + public void setSelectionFromTop(int position, int y) { + if (getAdapter() == null) { + return; + } + + setSelection(position); + //setSelectionInt(position); + + /*if (!isInTouchMode()) { + position = super.lookForSelectablePosition(position, true); + if (position >= 0) { + setNextSelectedPositionInt(position); + } + } else { + mResurrectToPosition = position; + }*/ + + /* + if (position >= 0) { + mLayoutMode = LAYOUT_SPECIFIC; + mSpecificTop = mListPadding.top + y; + + if (mNeedSync) { + mSyncPosition = position; + mSyncRowId = getAdapter().getItemId(position); + } + + if (mPositionScroller != null) { + mPositionScroller.stop(); + } + + requestLayout(); + } + */ + } + +} diff --git a/src/third_parties/in/srain/cube/lapache-2.0.txt b/src/third_parties/in/srain/cube/lapache-2.0.txt new file mode 100644 index 00000000..72f817fb --- /dev/null +++ b/src/third_parties/in/srain/cube/lapache-2.0.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/src/third_parties/michaelOrtiz/TouchImageViewCustom.java b/src/third_parties/michaelOrtiz/TouchImageViewCustom.java new file mode 100644 index 00000000..9b0f5a05 --- /dev/null +++ b/src/third_parties/michaelOrtiz/TouchImageViewCustom.java @@ -0,0 +1,1277 @@ +/** + * @author Michael Ortiz + * @updated Patrick Lackemacher + * @updated Babay88 + * @updated @ipsilondev + * @updated hank-cp + * @updated singpolyma + * Copyright (c) 2012 Michael Ortiz + */ + +package third_parties.michaelOrtiz; + +import com.owncloud.android.ui.preview.ImageViewCustom; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.OverScroller; +import android.widget.Scroller; + +/** + * Extends Android ImageView to include pinch zooming, panning, fling and double tap zoom. + */ +public class TouchImageViewCustom extends ImageViewCustom { + private static final String DEBUG = "DEBUG"; + + // + // SuperMin and SuperMax multipliers. Determine how much the image can be + // zoomed below or above the zoom boundaries, before animating back to the + // min/max zoom boundary. + // + private static final float SUPER_MIN_MULTIPLIER = .75f; + private static final float SUPER_MAX_MULTIPLIER = 1.25f; + + // + // Scale of image ranges from minScale to maxScale, where minScale == 1 + // when the image is stretched to fit view. + // + private float normalizedScale; + + // + // Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal. + // MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix + // saved prior to the screen rotating. + // + private Matrix matrix, prevMatrix; + + private static enum State { NONE, DRAG, ZOOM, FLING, ANIMATE_ZOOM }; + private State state; + + private float minScale; + private float maxScale; + private float superMinScale; + private float superMaxScale; + private float[] m; + + private Context context; + private Fling fling; + + private ScaleType mScaleType; + + private boolean imageRenderedAtLeastOnce; + private boolean onDrawReady; + + private ZoomVariables delayedZoomVariables; + + // + // Size of view and previous view size (ie before rotation) + // + private int viewWidth, viewHeight, prevViewWidth, prevViewHeight; + + // + // Size of image when it is stretched to fit view. Before and After rotation. + // + private float matchViewWidth, matchViewHeight, prevMatchViewWidth, prevMatchViewHeight; + + private ScaleGestureDetector mScaleDetector; + private GestureDetector mGestureDetector; + private GestureDetector.OnDoubleTapListener doubleTapListener = null; + private OnTouchListener userTouchListener = null; + private OnTouchImageViewListener touchImageViewListener = null; + + public TouchImageViewCustom(Context context) { + super(context); + sharedConstructing(context); + } + + public TouchImageViewCustom(Context context, AttributeSet attrs) { + super(context, attrs); + sharedConstructing(context); + } + + public TouchImageViewCustom(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + sharedConstructing(context); + } + + private void sharedConstructing(Context context) { + super.setClickable(true); + this.context = context; + mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); + mGestureDetector = new GestureDetector(context, new GestureListener()); + matrix = new Matrix(); + prevMatrix = new Matrix(); + m = new float[9]; + normalizedScale = 1; + if (mScaleType == null) { + mScaleType = ScaleType.FIT_CENTER; + } + minScale = 1; + maxScale = 3; + superMinScale = SUPER_MIN_MULTIPLIER * minScale; + superMaxScale = SUPER_MAX_MULTIPLIER * maxScale; + setImageMatrix(matrix); + setScaleType(ScaleType.MATRIX); + setState(State.NONE); + onDrawReady = false; + super.setOnTouchListener(new PrivateOnTouchListener()); + } + + @Override + public void setOnTouchListener(View.OnTouchListener l) { + userTouchListener = l; + } + + public void setOnTouchImageViewListener(OnTouchImageViewListener l) { + touchImageViewListener = l; + } + + public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener l) { + doubleTapListener = l; + } + + @Override + public void setImageResource(int resId) { + super.setImageResource(resId); + savePreviousImageValues(); + fitImageToView(); + } + + @Override + public void setImageBitmap(Bitmap bm) { + super.setImageBitmap(bm); + savePreviousImageValues(); + fitImageToView(); + } + + @Override + public void setImageDrawable(Drawable drawable) { + super.setImageDrawable(drawable); + savePreviousImageValues(); + fitImageToView(); + } + + @Override + public void setImageURI(Uri uri) { + super.setImageURI(uri); + savePreviousImageValues(); + fitImageToView(); + } + + @Override + public void setScaleType(ScaleType type) { + if (type == ScaleType.FIT_START || type == ScaleType.FIT_END) { + throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END"); + } + if (type == ScaleType.MATRIX) { + super.setScaleType(ScaleType.MATRIX); + + } else { + mScaleType = type; + if (onDrawReady) { + // + // If the image is already rendered, scaleType has been called programmatically + // and the TouchImageView should be updated with the new scaleType. + // + setZoom(this); + } + } + } + + @Override + public ScaleType getScaleType() { + return mScaleType; + } + + /** + * Returns false if image is in initial, unzoomed state. False, otherwise. + * @return true if image is zoomed + */ + public boolean isZoomed() { + return normalizedScale != 1; + } + + /** + * Return a Rect representing the zoomed image. + * @return rect representing zoomed image + */ + public RectF getZoomedRect() { + if (mScaleType == ScaleType.FIT_XY) { + throw new UnsupportedOperationException("getZoomedRect() not supported with FIT_XY"); + } + PointF topLeft = transformCoordTouchToBitmap(0, 0, true); + PointF bottomRight = transformCoordTouchToBitmap(viewWidth, viewHeight, true); + + float w = getDrawable().getIntrinsicWidth(); + float h = getDrawable().getIntrinsicHeight(); + return new RectF(topLeft.x / w, topLeft.y / h, bottomRight.x / w, bottomRight.y / h); + } + + /** + * Save the current matrix and view dimensions + * in the prevMatrix and prevView variables. + */ + private void savePreviousImageValues() { + if (matrix != null && viewHeight != 0 && viewWidth != 0) { + matrix.getValues(m); + prevMatrix.setValues(m); + prevMatchViewHeight = matchViewHeight; + prevMatchViewWidth = matchViewWidth; + prevViewHeight = viewHeight; + prevViewWidth = viewWidth; + } + } + + @Override + public Parcelable onSaveInstanceState() { + Bundle bundle = new Bundle(); + bundle.putParcelable("instanceState", super.onSaveInstanceState()); + bundle.putFloat("saveScale", normalizedScale); + bundle.putFloat("matchViewHeight", matchViewHeight); + bundle.putFloat("matchViewWidth", matchViewWidth); + bundle.putInt("viewWidth", viewWidth); + bundle.putInt("viewHeight", viewHeight); + matrix.getValues(m); + bundle.putFloatArray("matrix", m); + bundle.putBoolean("imageRendered", imageRenderedAtLeastOnce); + return bundle; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof Bundle) { + Bundle bundle = (Bundle) state; + normalizedScale = bundle.getFloat("saveScale"); + m = bundle.getFloatArray("matrix"); + prevMatrix.setValues(m); + prevMatchViewHeight = bundle.getFloat("matchViewHeight"); + prevMatchViewWidth = bundle.getFloat("matchViewWidth"); + prevViewHeight = bundle.getInt("viewHeight"); + prevViewWidth = bundle.getInt("viewWidth"); + imageRenderedAtLeastOnce = bundle.getBoolean("imageRendered"); + super.onRestoreInstanceState(bundle.getParcelable("instanceState")); + return; + } + + super.onRestoreInstanceState(state); + } + + @Override + protected void onDraw(Canvas canvas) { + onDrawReady = true; + imageRenderedAtLeastOnce = true; + if (delayedZoomVariables != null) { + setZoom(delayedZoomVariables.scale, delayedZoomVariables.focusX, delayedZoomVariables.focusY, delayedZoomVariables.scaleType); + delayedZoomVariables = null; + } + super.onDraw(canvas); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + savePreviousImageValues(); + } + + /** + * Get the max zoom multiplier. + * @return max zoom multiplier. + */ + public float getMaxZoom() { + return maxScale; + } + + /** + * Set the max zoom multiplier. Default value: 3. + * @param max max zoom multiplier. + */ + public void setMaxZoom(float max) { + maxScale = max; + superMaxScale = SUPER_MAX_MULTIPLIER * maxScale; + } + + /** + * Get the min zoom multiplier. + * @return min zoom multiplier. + */ + public float getMinZoom() { + return minScale; + } + + /** + * Get the current zoom. This is the zoom relative to the initial + * scale, not the original resource. + * @return current zoom multiplier. + */ + public float getCurrentZoom() { + return normalizedScale; + } + + /** + * Set the min zoom multiplier. Default value: 1. + * @param min min zoom multiplier. + */ + public void setMinZoom(float min) { + minScale = min; + superMinScale = SUPER_MIN_MULTIPLIER * minScale; + } + + /** + * Reset zoom and translation to initial state. + */ + public void resetZoom() { + normalizedScale = 1; + fitImageToView(); + } + + /** + * Set zoom to the specified scale. Image will be centered by default. + * @param scale + */ + public void setZoom(float scale) { + setZoom(scale, 0.5f, 0.5f); + } + + /** + * Set zoom to the specified scale. Image will be centered around the point + * (focusX, focusY). These floats range from 0 to 1 and denote the focus point + * as a fraction from the left and top of the view. For example, the top left + * corner of the image would be (0, 0). And the bottom right corner would be (1, 1). + * @param scale + * @param focusX + * @param focusY + */ + public void setZoom(float scale, float focusX, float focusY) { + setZoom(scale, focusX, focusY, mScaleType); + } + + /** + * Set zoom to the specified scale. Image will be centered around the point + * (focusX, focusY). These floats range from 0 to 1 and denote the focus point + * as a fraction from the left and top of the view. For example, the top left + * corner of the image would be (0, 0). And the bottom right corner would be (1, 1). + * @param scale + * @param focusX + * @param focusY + * @param scaleType + */ + public void setZoom(float scale, float focusX, float focusY, ScaleType scaleType) { + // + // setZoom can be called before the image is on the screen, but at this point, + // image and view sizes have not yet been calculated in onMeasure. Thus, we should + // delay calling setZoom until the view has been measured. + // + if (!onDrawReady) { + delayedZoomVariables = new ZoomVariables(scale, focusX, focusY, scaleType); + return; + } + + if (scaleType != mScaleType) { + setScaleType(scaleType); + } + resetZoom(); + scaleImage(scale, viewWidth / 2, viewHeight / 2, true); + matrix.getValues(m); + m[Matrix.MTRANS_X] = -((focusX * getImageWidth()) - (viewWidth * 0.5f)); + m[Matrix.MTRANS_Y] = -((focusY * getImageHeight()) - (viewHeight * 0.5f)); + matrix.setValues(m); + fixTrans(); + setImageMatrix(matrix); + } + + /** + * Set zoom parameters equal to another TouchImageView. Including scale, position, + * and ScaleType. + * @param img + */ + public void setZoom(TouchImageViewCustom img) { + PointF center = img.getScrollPosition(); + setZoom(img.getCurrentZoom(), center.x, center.y, img.getScaleType()); + } + + /** + * Return the point at the center of the zoomed image. The PointF coordinates range + * in value between 0 and 1 and the focus point is denoted as a fraction from the left + * and top of the view. For example, the top left corner of the image would be (0, 0). + * And the bottom right corner would be (1, 1). + * @return PointF representing the scroll position of the zoomed image. + */ + public PointF getScrollPosition() { + Drawable drawable = getDrawable(); + if (drawable == null) { + return null; + } + int drawableWidth = drawable.getIntrinsicWidth(); + int drawableHeight = drawable.getIntrinsicHeight(); + + PointF point = transformCoordTouchToBitmap(viewWidth / 2, viewHeight / 2, true); + point.x /= drawableWidth; + point.y /= drawableHeight; + return point; + } + + /** + * Set the focus point of the zoomed image. The focus points are denoted as a fraction from the + * left and top of the view. The focus points can range in value between 0 and 1. + * @param focusX + * @param focusY + */ + public void setScrollPosition(float focusX, float focusY) { + setZoom(normalizedScale, focusX, focusY); + } + + /** + * Performs boundary checking and fixes the image matrix if it + * is out of bounds. + */ + private void fixTrans() { + matrix.getValues(m); + float transX = m[Matrix.MTRANS_X]; + float transY = m[Matrix.MTRANS_Y]; + + float fixTransX = getFixTrans(transX, viewWidth, getImageWidth()); + float fixTransY = getFixTrans(transY, viewHeight, getImageHeight()); + + if (fixTransX != 0 || fixTransY != 0) { + matrix.postTranslate(fixTransX, fixTransY); + } + } + + /** + * When transitioning from zooming from focus to zoom from center (or vice versa) + * the image can become unaligned within the view. This is apparent when zooming + * quickly. When the content size is less than the view size, the content will often + * be centered incorrectly within the view. fixScaleTrans first calls fixTrans() and + * then makes sure the image is centered correctly within the view. + */ + private void fixScaleTrans() { + fixTrans(); + matrix.getValues(m); + if (getImageWidth() < viewWidth) { + m[Matrix.MTRANS_X] = (viewWidth - getImageWidth()) / 2; + } + + if (getImageHeight() < viewHeight) { + m[Matrix.MTRANS_Y] = (viewHeight - getImageHeight()) / 2; + } + matrix.setValues(m); + } + + private float getFixTrans(float trans, float viewSize, float contentSize) { + float minTrans, maxTrans; + + if (contentSize <= viewSize) { + minTrans = 0; + maxTrans = viewSize - contentSize; + + } else { + minTrans = viewSize - contentSize; + maxTrans = 0; + } + + if (trans < minTrans) + return -trans + minTrans; + if (trans > maxTrans) + return -trans + maxTrans; + return 0; + } + + private float getFixDragTrans(float delta, float viewSize, float contentSize) { + if (contentSize <= viewSize) { + return 0; + } + return delta; + } + + private float getImageWidth() { + return matchViewWidth * normalizedScale; + } + + private float getImageHeight() { + return matchViewHeight * normalizedScale; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + Drawable drawable = getDrawable(); + if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) { + setMeasuredDimension(0, 0); + return; + } + + int drawableWidth = drawable.getIntrinsicWidth(); + int drawableHeight = drawable.getIntrinsicHeight(); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + viewWidth = setViewSize(widthMode, widthSize, drawableWidth); + viewHeight = setViewSize(heightMode, heightSize, drawableHeight); + + // + // Set view dimensions + // + setMeasuredDimension(viewWidth, viewHeight); + + // + // Fit content within view + // + fitImageToView(); + } + + /** + * If the normalizedScale is equal to 1, then the image is made to fit the screen. Otherwise, + * it is made to fit the screen according to the dimensions of the previous image matrix. This + * allows the image to maintain its zoom after rotation. + */ + private void fitImageToView() { + Drawable drawable = getDrawable(); + if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) { + return; + } + if (matrix == null || prevMatrix == null) { + return; + } + + int drawableWidth = drawable.getIntrinsicWidth(); + int drawableHeight = drawable.getIntrinsicHeight(); + + // + // Scale image for view + // + float scaleX = (float) viewWidth / drawableWidth; + float scaleY = (float) viewHeight / drawableHeight; + + switch (mScaleType) { + case CENTER: + scaleX = scaleY = 1; + break; + + case CENTER_CROP: + scaleX = scaleY = Math.max(scaleX, scaleY); + break; + + case CENTER_INSIDE: + scaleX = scaleY = Math.min(1, Math.min(scaleX, scaleY)); + + case FIT_CENTER: + scaleX = scaleY = Math.min(scaleX, scaleY); + break; + + case FIT_XY: + break; + + default: + // + // FIT_START and FIT_END not supported + // + throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END"); + + } + + // + // Center the image + // + float redundantXSpace = viewWidth - (scaleX * drawableWidth); + float redundantYSpace = viewHeight - (scaleY * drawableHeight); + matchViewWidth = viewWidth - redundantXSpace; + matchViewHeight = viewHeight - redundantYSpace; + if (!isZoomed() && !imageRenderedAtLeastOnce) { + // + // Stretch and center image to fit view + // + matrix.setScale(scaleX, scaleY); + matrix.postTranslate(redundantXSpace / 2, redundantYSpace / 2); + normalizedScale = 1; + + } else { + // + // These values should never be 0 or we will set viewWidth and viewHeight + // to NaN in translateMatrixAfterRotate. To avoid this, call savePreviousImageValues + // to set them equal to the current values. + // + if (prevMatchViewWidth == 0 || prevMatchViewHeight == 0) { + savePreviousImageValues(); + } + + prevMatrix.getValues(m); + + // + // Rescale Matrix after rotation + // + m[Matrix.MSCALE_X] = matchViewWidth / drawableWidth * normalizedScale; + m[Matrix.MSCALE_Y] = matchViewHeight / drawableHeight * normalizedScale; + + // + // TransX and TransY from previous matrix + // + float transX = m[Matrix.MTRANS_X]; + float transY = m[Matrix.MTRANS_Y]; + + // + // Width + // + float prevActualWidth = prevMatchViewWidth * normalizedScale; + float actualWidth = getImageWidth(); + translateMatrixAfterRotate(Matrix.MTRANS_X, transX, prevActualWidth, actualWidth, prevViewWidth, viewWidth, drawableWidth); + + // + // Height + // + float prevActualHeight = prevMatchViewHeight * normalizedScale; + float actualHeight = getImageHeight(); + translateMatrixAfterRotate(Matrix.MTRANS_Y, transY, prevActualHeight, actualHeight, prevViewHeight, viewHeight, drawableHeight); + + // + // Set the matrix to the adjusted scale and translate values. + // + matrix.setValues(m); + } + fixTrans(); + setImageMatrix(matrix); + } + + /** + * Set view dimensions based on layout params + * + * @param mode + * @param size + * @param drawableWidth + * @return + */ + private int setViewSize(int mode, int size, int drawableWidth) { + int viewSize; + switch (mode) { + case MeasureSpec.EXACTLY: + viewSize = size; + break; + + case MeasureSpec.AT_MOST: + viewSize = Math.min(drawableWidth, size); + break; + + case MeasureSpec.UNSPECIFIED: + viewSize = drawableWidth; + break; + + default: + viewSize = size; + break; + } + return viewSize; + } + + /** + * After rotating, the matrix needs to be translated. This function finds the area of image + * which was previously centered and adjusts translations so that is again the center, post-rotation. + * + * @param axis Matrix.MTRANS_X or Matrix.MTRANS_Y + * @param trans the value of trans in that axis before the rotation + * @param prevImageSize the width/height of the image before the rotation + * @param imageSize width/height of the image after rotation + * @param prevViewSize width/height of view before rotation + * @param viewSize width/height of view after rotation + * @param drawableSize width/height of drawable + */ + private void translateMatrixAfterRotate(int axis, float trans, float prevImageSize, float imageSize, int prevViewSize, int viewSize, int drawableSize) { + if (imageSize < viewSize) { + // + // The width/height of image is less than the view's width/height. Center it. + // + m[axis] = (viewSize - (drawableSize * m[Matrix.MSCALE_X])) * 0.5f; + + } else if (trans > 0) { + // + // The image is larger than the view, but was not before rotation. Center it. + // + m[axis] = -((imageSize - viewSize) * 0.5f); + + } else { + // + // Find the area of the image which was previously centered in the view. Determine its distance + // from the left/top side of the view as a fraction of the entire image's width/height. Use that percentage + // to calculate the trans in the new view width/height. + // + float percentage = (Math.abs(trans) + (0.5f * prevViewSize)) / prevImageSize; + m[axis] = -((percentage * imageSize) - (viewSize * 0.5f)); + } + } + + private void setState(State state) { + this.state = state; + } + + public boolean canScrollHorizontallyFroyo(int direction) { + return canScrollHorizontally(direction); + } + + @Override + public boolean canScrollHorizontally(int direction) { + matrix.getValues(m); + float x = m[Matrix.MTRANS_X]; + + if (getImageWidth() < viewWidth) { + return false; + + } else if (x >= -1 && direction < 0) { + return false; + + } else if (Math.abs(x) + viewWidth + 1 >= getImageWidth() && direction > 0) { + return false; + } + + return true; + } + + /** + * Gesture Listener detects a single click or long click and passes that on + * to the view's listener. + * @author Ortiz + * + */ + private class GestureListener extends GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) + { + if(doubleTapListener != null) { + return doubleTapListener.onSingleTapConfirmed(e); + } + return performClick(); + } + + @Override + public void onLongPress(MotionEvent e) + { + performLongClick(); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) + { + if (fling != null) { + // + // If a previous fling is still active, it should be cancelled so that two flings + // are not run simultaenously. + // + fling.cancelFling(); + } + fling = new Fling((int) velocityX, (int) velocityY); + compatPostOnAnimation(fling); + return super.onFling(e1, e2, velocityX, velocityY); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + boolean consumed = false; + if(doubleTapListener != null) { + consumed = doubleTapListener.onDoubleTap(e); + } + if (state == State.NONE) { + float targetZoom = (normalizedScale == minScale) ? maxScale : minScale; + DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, e.getX(), e.getY(), false); + compatPostOnAnimation(doubleTap); + consumed = true; + } + return consumed; + } + + @Override + public boolean onDoubleTapEvent(MotionEvent e) { + if(doubleTapListener != null) { + return doubleTapListener.onDoubleTapEvent(e); + } + return false; + } + } + + public interface OnTouchImageViewListener { + public void onMove(); + } + + /** + * Responsible for all touch events. Handles the heavy lifting of drag and also sends + * touch events to Scale Detector and Gesture Detector. + * @author Ortiz + * + */ + private class PrivateOnTouchListener implements OnTouchListener { + + // + // Remember last point position for dragging + // + private PointF last = new PointF(); + + @Override + public boolean onTouch(View v, MotionEvent event) { + mScaleDetector.onTouchEvent(event); + mGestureDetector.onTouchEvent(event); + PointF curr = new PointF(event.getX(), event.getY()); + + if (state == State.NONE || state == State.DRAG || state == State.FLING) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + last.set(curr); + if (fling != null) + fling.cancelFling(); + setState(State.DRAG); + break; + + case MotionEvent.ACTION_MOVE: + if (state == State.DRAG) { + float deltaX = curr.x - last.x; + float deltaY = curr.y - last.y; + float fixTransX = getFixDragTrans(deltaX, viewWidth, getImageWidth()); + float fixTransY = getFixDragTrans(deltaY, viewHeight, getImageHeight()); + matrix.postTranslate(fixTransX, fixTransY); + fixTrans(); + last.set(curr.x, curr.y); + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + setState(State.NONE); + break; + } + } + + setImageMatrix(matrix); + + // + // User-defined OnTouchListener + // + if(userTouchListener != null) { + userTouchListener.onTouch(v, event); + } + + // + // OnTouchImageViewListener is set: TouchImageView dragged by user. + // + if (touchImageViewListener != null) { + touchImageViewListener.onMove(); + } + + // + // indicate event was handled + // + return true; + } + } + + /** + * ScaleListener detects user two finger scaling and scales image. + * @author Ortiz + * + */ + private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + setState(State.ZOOM); + return true; + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + scaleImage(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY(), true); + + // + // OnTouchImageViewListener is set: TouchImageView pinch zoomed by user. + // + if (touchImageViewListener != null) { + touchImageViewListener.onMove(); + } + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + super.onScaleEnd(detector); + setState(State.NONE); + boolean animateToZoomBoundary = false; + float targetZoom = normalizedScale; + if (normalizedScale > maxScale) { + targetZoom = maxScale; + animateToZoomBoundary = true; + + } else if (normalizedScale < minScale) { + targetZoom = minScale; + animateToZoomBoundary = true; + } + + if (animateToZoomBoundary) { + DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, viewWidth / 2, viewHeight / 2, true); + compatPostOnAnimation(doubleTap); + } + } + } + + private void scaleImage(double deltaScale, float focusX, float focusY, boolean stretchImageToSuper) { + + float lowerScale, upperScale; + if (stretchImageToSuper) { + lowerScale = superMinScale; + upperScale = superMaxScale; + + } else { + lowerScale = minScale; + upperScale = maxScale; + } + + float origScale = normalizedScale; + normalizedScale *= deltaScale; + if (normalizedScale > upperScale) { + normalizedScale = upperScale; + deltaScale = upperScale / origScale; + } else if (normalizedScale < lowerScale) { + normalizedScale = lowerScale; + deltaScale = lowerScale / origScale; + } + + matrix.postScale((float) deltaScale, (float) deltaScale, focusX, focusY); + fixScaleTrans(); + } + + /** + * DoubleTapZoom calls a series of runnables which apply + * an animated zoom in/out graphic to the image. + * @author Ortiz + * + */ + private class DoubleTapZoom implements Runnable { + + private long startTime; + private static final float ZOOM_TIME = 500; + private float startZoom, targetZoom; + private float bitmapX, bitmapY; + private boolean stretchImageToSuper; + private AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator(); + private PointF startTouch; + private PointF endTouch; + + DoubleTapZoom(float targetZoom, float focusX, float focusY, boolean stretchImageToSuper) { + setState(State.ANIMATE_ZOOM); + startTime = System.currentTimeMillis(); + this.startZoom = normalizedScale; + this.targetZoom = targetZoom; + this.stretchImageToSuper = stretchImageToSuper; + PointF bitmapPoint = transformCoordTouchToBitmap(focusX, focusY, false); + this.bitmapX = bitmapPoint.x; + this.bitmapY = bitmapPoint.y; + + // + // Used for translating image during scaling + // + startTouch = transformCoordBitmapToTouch(bitmapX, bitmapY); + endTouch = new PointF(viewWidth / 2, viewHeight / 2); + } + + @Override + public void run() { + float t = interpolate(); + double deltaScale = calculateDeltaScale(t); + scaleImage(deltaScale, bitmapX, bitmapY, stretchImageToSuper); + translateImageToCenterTouchPosition(t); + fixScaleTrans(); + setImageMatrix(matrix); + + // + // OnTouchImageViewListener is set: double tap runnable updates listener + // with every frame. + // + if (touchImageViewListener != null) { + touchImageViewListener.onMove(); + } + + if (t < 1f) { + // + // We haven't finished zooming + // + compatPostOnAnimation(this); + + } else { + // + // Finished zooming + // + setState(State.NONE); + } + } + + /** + * Interpolate between where the image should start and end in order to translate + * the image so that the point that is touched is what ends up centered at the end + * of the zoom. + * @param t + */ + private void translateImageToCenterTouchPosition(float t) { + float targetX = startTouch.x + t * (endTouch.x - startTouch.x); + float targetY = startTouch.y + t * (endTouch.y - startTouch.y); + PointF curr = transformCoordBitmapToTouch(bitmapX, bitmapY); + matrix.postTranslate(targetX - curr.x, targetY - curr.y); + } + + /** + * Use interpolator to get t + * @return + */ + private float interpolate() { + long currTime = System.currentTimeMillis(); + float elapsed = (currTime - startTime) / ZOOM_TIME; + elapsed = Math.min(1f, elapsed); + return interpolator.getInterpolation(elapsed); + } + + /** + * Interpolate the current targeted zoom and get the delta + * from the current zoom. + * @param t + * @return + */ + private double calculateDeltaScale(float t) { + double zoom = startZoom + t * (targetZoom - startZoom); + return zoom / normalizedScale; + } + } + + /** + * This function will transform the coordinates in the touch event to the coordinate + * system of the drawable that the imageview contain + * @param x x-coordinate of touch event + * @param y y-coordinate of touch event + * @param clipToBitmap Touch event may occur within view, but outside image content. True, to clip return value + * to the bounds of the bitmap size. + * @return Coordinates of the point touched, in the coordinate system of the original drawable. + */ + private PointF transformCoordTouchToBitmap(float x, float y, boolean clipToBitmap) { + matrix.getValues(m); + float origW = getDrawable().getIntrinsicWidth(); + float origH = getDrawable().getIntrinsicHeight(); + float transX = m[Matrix.MTRANS_X]; + float transY = m[Matrix.MTRANS_Y]; + float finalX = ((x - transX) * origW) / getImageWidth(); + float finalY = ((y - transY) * origH) / getImageHeight(); + + if (clipToBitmap) { + finalX = Math.min(Math.max(finalX, 0), origW); + finalY = Math.min(Math.max(finalY, 0), origH); + } + + return new PointF(finalX , finalY); + } + + /** + * Inverse of transformCoordTouchToBitmap. This function will transform the coordinates in the + * drawable's coordinate system to the view's coordinate system. + * @param bx x-coordinate in original bitmap coordinate system + * @param by y-coordinate in original bitmap coordinate system + * @return Coordinates of the point in the view's coordinate system. + */ + private PointF transformCoordBitmapToTouch(float bx, float by) { + matrix.getValues(m); + float origW = getDrawable().getIntrinsicWidth(); + float origH = getDrawable().getIntrinsicHeight(); + float px = bx / origW; + float py = by / origH; + float finalX = m[Matrix.MTRANS_X] + getImageWidth() * px; + float finalY = m[Matrix.MTRANS_Y] + getImageHeight() * py; + return new PointF(finalX , finalY); + } + + /** + * Fling launches sequential runnables which apply + * the fling graphic to the image. The values for the translation + * are interpolated by the Scroller. + * @author Ortiz + * + */ + private class Fling implements Runnable { + + CompatScroller scroller; + int currX, currY; + + Fling(int velocityX, int velocityY) { + setState(State.FLING); + scroller = new CompatScroller(context); + matrix.getValues(m); + + int startX = (int) m[Matrix.MTRANS_X]; + int startY = (int) m[Matrix.MTRANS_Y]; + int minX, maxX, minY, maxY; + + if (getImageWidth() > viewWidth) { + minX = viewWidth - (int) getImageWidth(); + maxX = 0; + + } else { + minX = maxX = startX; + } + + if (getImageHeight() > viewHeight) { + minY = viewHeight - (int) getImageHeight(); + maxY = 0; + + } else { + minY = maxY = startY; + } + + scroller.fling(startX, startY, (int) velocityX, (int) velocityY, minX, + maxX, minY, maxY); + currX = startX; + currY = startY; + } + + public void cancelFling() { + if (scroller != null) { + setState(State.NONE); + scroller.forceFinished(true); + } + } + + @Override + public void run() { + + // + // OnTouchImageViewListener is set: TouchImageView listener has been flung by user. + // Listener runnable updated with each frame of fling animation. + // + if (touchImageViewListener != null) { + touchImageViewListener.onMove(); + } + + if (scroller.isFinished()) { + scroller = null; + return; + } + + if (scroller.computeScrollOffset()) { + int newX = scroller.getCurrX(); + int newY = scroller.getCurrY(); + int transX = newX - currX; + int transY = newY - currY; + currX = newX; + currY = newY; + matrix.postTranslate(transX, transY); + fixTrans(); + setImageMatrix(matrix); + compatPostOnAnimation(this); + } + } + } + + @TargetApi(Build.VERSION_CODES.GINGERBREAD) + private class CompatScroller { + Scroller scroller; + OverScroller overScroller; + boolean isPreGingerbread; + + public CompatScroller(Context context) { + if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD) { + isPreGingerbread = true; + scroller = new Scroller(context); + + } else { + isPreGingerbread = false; + overScroller = new OverScroller(context); + } + } + + public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) { + if (isPreGingerbread) { + scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); + } else { + overScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); + } + } + + public void forceFinished(boolean finished) { + if (isPreGingerbread) { + scroller.forceFinished(finished); + } else { + overScroller.forceFinished(finished); + } + } + + public boolean isFinished() { + if (isPreGingerbread) { + return scroller.isFinished(); + } else { + return overScroller.isFinished(); + } + } + + public boolean computeScrollOffset() { + if (isPreGingerbread) { + return scroller.computeScrollOffset(); + } else { + overScroller.computeScrollOffset(); + return overScroller.computeScrollOffset(); + } + } + + public int getCurrX() { + if (isPreGingerbread) { + return scroller.getCurrX(); + } else { + return overScroller.getCurrX(); + } + } + + public int getCurrY() { + if (isPreGingerbread) { + return scroller.getCurrY(); + } else { + return overScroller.getCurrY(); + } + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private void compatPostOnAnimation(Runnable runnable) { + if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { + postOnAnimation(runnable); + + } else { + postDelayed(runnable, 1000/60); + } + } + + private class ZoomVariables { + public float scale; + public float focusX; + public float focusY; + public ScaleType scaleType; + + public ZoomVariables(float scale, float focusX, float focusY, ScaleType scaleType) { + this.scale = scale; + this.focusX = focusX; + this.focusY = focusY; + this.scaleType = scaleType; + } + } + + private void printMatrixInfo() { + float[] n = new float[9]; + matrix.getValues(n); + Log.d(DEBUG, "Scale: " + n[Matrix.MSCALE_X] + " TransX: " + n[Matrix.MTRANS_X] + " TransY: " + n[Matrix.MTRANS_Y]); + } +} \ No newline at end of file diff --git a/tests/src/com/owncloud/android/test/AccountUtilsTest.java b/tests/src/com/owncloud/android/test/AccountUtilsTest.java index fc6dc212..769b98b2 100644 --- a/tests/src/com/owncloud/android/test/AccountUtilsTest.java +++ b/tests/src/com/owncloud/android/test/AccountUtilsTest.java @@ -1,6 +1,8 @@ -/* ownCloud Android client application +/** + * ownCloud Android client application + * * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. + * 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, diff --git a/user_manual/Makefile b/user_manual/Makefile new file mode 100644 index 00000000..74c47b13 --- /dev/null +++ b/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 ' where 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." diff --git a/user_manual/android_app.rst b/user_manual/android_app.rst new file mode 100644 index 00000000..de2b2d57 --- /dev/null +++ b/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 +`_. + +.. 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 +`_, and get source code and +more information from the `ownCloud download page +`_. + +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 diff --git a/user_manual/conf.py b/user_manual/conf.py new file mode 100644 index 00000000..20e5bda6 --- /dev/null +++ b/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 +# " v 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 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 diff --git a/user_manual/images/android-downloads.png b/user_manual/images/android-downloads.png new file mode 100644 index 00000000..e0bb5454 Binary files /dev/null and b/user_manual/images/android-downloads.png differ diff --git a/user_manual/images/android-file-list.png b/user_manual/images/android-file-list.png new file mode 100644 index 00000000..e479b9f0 Binary files /dev/null and b/user_manual/images/android-file-list.png differ diff --git a/user_manual/images/android-file-options.png b/user_manual/images/android-file-options.png new file mode 100644 index 00000000..12867a31 Binary files /dev/null and b/user_manual/images/android-file-options.png differ diff --git a/user_manual/images/android-file.png b/user_manual/images/android-file.png new file mode 100644 index 00000000..89ffd28b Binary files /dev/null and b/user_manual/images/android-file.png differ diff --git a/user_manual/images/android-files-page.png b/user_manual/images/android-files-page.png new file mode 100644 index 00000000..1c7fc4dd Binary files /dev/null and b/user_manual/images/android-files-page.png differ diff --git a/user_manual/images/android-first-screen.jpg b/user_manual/images/android-first-screen.jpg new file mode 100644 index 00000000..f4c5132a Binary files /dev/null and b/user_manual/images/android-first-screen.jpg differ diff --git a/user_manual/images/android-help.png b/user_manual/images/android-help.png new file mode 100644 index 00000000..56a7464f Binary files /dev/null and b/user_manual/images/android-help.png differ diff --git a/user_manual/images/android-new-account.png b/user_manual/images/android-new-account.png new file mode 100644 index 00000000..ffbe12e5 Binary files /dev/null and b/user_manual/images/android-new-account.png differ diff --git a/user_manual/images/android-settings.png b/user_manual/images/android-settings.png new file mode 100644 index 00000000..7a160845 Binary files /dev/null and b/user_manual/images/android-settings.png differ diff --git a/user_manual/images/android-ssl-cert.png b/user_manual/images/android-ssl-cert.png new file mode 100644 index 00000000..9286fe62 Binary files /dev/null and b/user_manual/images/android-ssl-cert.png differ diff --git a/user_manual/images/android-upload.png b/user_manual/images/android-upload.png new file mode 100644 index 00000000..6ee1b952 Binary files /dev/null and b/user_manual/images/android-upload.png differ diff --git a/user_manual/index.rst b/user_manual/index.rst new file mode 100644 index 00000000..cf159125 --- /dev/null +++ b/user_manual/index.rst @@ -0,0 +1,9 @@ +.. _contents: + +ownCloud Android App Manual +============================== + +.. toctree:: + :maxdepth: 2 + + android_app diff --git a/user_manual/make.bat b/user_manual/make.bat new file mode 100644 index 00000000..3fbb57f5 --- /dev/null +++ b/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 ^` where ^ 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 diff --git a/user_manual/ocdoc b/user_manual/ocdoc new file mode 160000 index 00000000..343496c7 --- /dev/null +++ b/user_manual/ocdoc @@ -0,0 +1 @@ +Subproject commit 343496c792616459e8204b6614fd42a1b16a6d68