diff --git a/.travis.yml b/.travis.yml index d205021d0d..50b73d1b98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,20 @@ -language: java +language: android +android: + components: + - build-tools-20.0.0 + - android-19 + - android-17 + - android-14 + - extra-android-support + licenses: + - 'android-sdk-license-5be876d5' + - 'android-sdk-license-598b93a6' + jdk: oraclejdk7 + before_install: - # Install base Android SDK - - sudo apt-get update -qq - - sudo apt-get install -qq libstdc++6:i386 lib32z1 expect - - export COMPONENTS="build-tools-20.0.0,android-14,android-17,android-19,sysimg-19,extra-android-support" - - export LICENSES="android-sdk-license-5be876d5|android-sdk-license-598b93a6" - - curl -3L https://raw.github.com/embarkmobile/android-sdk-installer/version-2/android-sdk-installer | bash /dev/stdin --install=$COMPONENTS --accept=$LICENSES - - source ~/.android-sdk-installer/env - - rm pom.xml - - ./setup_env.sh + - rm pom.xml + - ./setup_env.sh script: - ant clean diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 2671cce211..910ea787f7 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -18,8 +18,8 @@ along with this program. If not, see . --> + android:versionCode="10600000" + android:versionName="1.6.0" xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/SETUP.md b/SETUP.md index a965064413..39539038e9 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,10 +1,14 @@ -If you want to start help developing ownCloud please follow the [contribution guidelines][0] and observe these instructions: - -### 1. Fork and download android/develop repository: +If you want to start help developing ownCloud please follow the [contribution guidelines][0] and observe these instructions. -NOTE: You must have git in your environment path variable to perform the next operations. +If you have any problems, start again with 1) and work your way down. If something still does not work as described here, please open a new issue describing exactly what you did, what happened, and what should have happened. +### 1) Fork and download android/develop repository: + +NOTE: Android SDK with platforms 8, 14 and 19 (and maybe others) need to be installed. + You must have the Android SDK 'tools/', and 'platforms-tools/' folders in your environment path variable. + "git" need to be installed and in your environment path variable. + * Navigate to https://github.com/owncloud/android, click fork. * Clone your new repo: "git clone git@github.com:YOURGITHUBNAME/android.git" * Move to the project folder with "cd android" @@ -14,16 +18,16 @@ NOTE: You must have git in your environment path variable to perform the next op * Make sure to get the latest changes from official android/develop branch: "git pull upstream develop" * Complete the setup of project properties and resolve pending dependencies running "setup_env.bat" or "./setup_env.sh" . -At this point you can continue using different tools to build the project. Section 2, 3 and 4 describe some of the existing alternatives. +At this point you can continue using different tools to build the project. Sections 2a), 2b), and 2c) describe some of the existing alternatives. -### 2. Building with Ant: +### 2a) Building with Ant: NOTE: You must have the Android SDK 'tools/', and 'platforms-tools/' folders in your environment path variable. * Run "ant clean" . * Run "ant debug" to generate a debuggable version of the ownCloud app. -### 3. Building with console/maven: +### 2b) Building with console/maven: NOTE: You must have mvn (version >= 3.1.1) in your environment path. Current Android 'platforms-tools' need to be installed. @@ -39,7 +43,7 @@ Download/install Android plugin for Maven, install owncloud-android-library, the Now you can create ownCloud APK using "mvn package" -### 4. Building with Eclipse: +### 2c) Building with Eclipse: NOTE: You must have the Android SDK 'tools/', and 'platforms-tools/' folders in your environment path variable. @@ -47,11 +51,12 @@ NOTE: You must have the Android SDK 'tools/', and 'platforms-tools/' folders in * Open Eclipse and create new "Android Project from Existing Code". Choose android/actionbarsherlock/library as root. * Clean project and compile. * If any error appear, check the project properties; in the 'Android' section, API Level should be greater or equal than 14. +* If "error loading libz.so.1" appears, try "sudo apt-get install lib32z1" * Make sure android/actionbarsherlock/library/bin/library.jar was created. -* Create a new "Android Project from Existing Code". Choose android/owncloud-android-library as root. +* Create a new "Android Project from Existing Code". Choose android/owncloud-android-library as root. (test and sample clients are not required.) * Clean project and compile. * If any error appear, check the project properties; in the 'Android' section, API Level should be 19 or greater. -* Make sure android/owncloud-android-library/bin/classes.jar was created. +* Make sure 'android/owncloud-android-library/bin/owncloud android library.jar' was created. * Import ownCloud Android project. * Clean project and compile. * If any error appears, check the project properties of owncloud-android project; in the 'Android' section: @@ -61,7 +66,7 @@ NOTE: You must have the Android SDK 'tools/', and 'platforms-tools/' folders in NOTE: Even though API level is set to 19, APK also runs on older devices because in AndroidManifest.xml minSdkVersion is set to 8. -### 5. Create pull request: +### 3) Create pull request: NOTE: You must sign the [Contributor Agreement][1] before your changes can be accepted! @@ -72,7 +77,7 @@ NOTE: You must sign the [Contributor Agreement][1] before your changes can be ac * Again, click "Edit" and set "compare:develop" * Enter description and send pull request. -### 6. Create another pull request: +### 4) Create another pull request: To make sure your new pull request does not contain commits which are already contained in previous PRs, create a new branch which is a clone of upstream/develop. diff --git a/oc_jb_workaround/AndroidManifest.xml b/oc_jb_workaround/AndroidManifest.xml index 9fa39b308d..b8b89c1ef1 100644 --- a/oc_jb_workaround/AndroidManifest.xml +++ b/oc_jb_workaround/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="0100018" + android:versionName="1.0.18" > . --> - حسابات كلمة مرور خاطئة اختيار + الأمان diff --git a/res/values-bg-rBG/strings.xml b/res/values-bg-rBG/strings.xml index a00871f2a2..47d243cf46 100644 --- a/res/values-bg-rBG/strings.xml +++ b/res/values-bg-rBG/strings.xml @@ -263,7 +263,6 @@ Файлът вече не се намира на този сървър Профили Добавяне на профил - Сигурна връзка е пренасочена по несигурен път. Доклади Изпрати История ownCloud Android доклади @@ -278,4 +277,5 @@ Файлът вече съществува в отдалечената папка. Настъпи грешка при опита за преместване на този файл или папка. за да преместиш този файл + Сигурност diff --git a/res/values-bn-rBD/strings.xml b/res/values-bn-rBD/strings.xml index 74739f5dae..cae19b70e6 100644 --- a/res/values-bn-rBD/strings.xml +++ b/res/values-bn-rBD/strings.xml @@ -259,10 +259,10 @@ সার্ভারে এই ফাইলটি আর প্রাপ্তব্য নয় একাউন্ট একাউন্ট যোগ কর - নিরাপদ সংযোগকে একটি অনিরাপদ পথে দিকবদল করা হয়েছে ভুল কুটশব্দ সরাও এখানে কিছু নেই। একটি ফোল্ডার যোগ করতে পারেন! বেছে নিন সরাতে ব্যার্থ হলো। ফাইলটি রয়েছে কিনা দেখুন। + নিরাপত্তা diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index 9c1d158ef5..d0b98a1d33 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -257,8 +257,8 @@ El fitxer ja no està disponible en el servidor Comptes Afegeix compte - La connexió segura està essent redirigida a través d\'una ruta insegura Es requereix autenticació Contrasenya incorrecta Escull + Seguretat diff --git a/res/values-cs-rCZ/strings.xml b/res/values-cs-rCZ/strings.xml index 71b84c9ee4..6b8b96c6f1 100644 --- a/res/values-cs-rCZ/strings.xml +++ b/res/values-cs-rCZ/strings.xml @@ -15,9 +15,9 @@ Více Účty Spravovat účty - PIN aplikace + PIN do aplikace Chraňte svého klienta - Okamžité nahrání obrázků + Okamžité nahrávání obrázků Okamžitě nahrávat vytvořené fotografie Okamžité nahrávání videa Okamžitě odesílat nahrané video @@ -27,7 +27,7 @@ Zobrazuje zaznamenané logy Smazat historii Nápověda - Doporučit příteli + Doporučit přátelům Odezva Imprint Zkuste %1$s na vašem smartphonu! @@ -226,7 +226,7 @@ 389 KB 2012/05/18 12:23 PM 12:23:45 - Odesílat obrázky pouze skrze WiFi + Odesílat obrázky pouze přes WiFi Nahrávat videa pouze přes WiFi /InstantUpload Konflikt při aktualizaci @@ -237,6 +237,7 @@ Náhled obrázku Obrázek nelze zobrazit %1$s nelze zkopírovat do místního adresáře %2$s + Cesta pro nahrání Je nám líto, ale sdílení není na vašem serveru povoleno. Kontaktujte svého administrátora. Nelze sdílet. Zkontrolujte prosím že soubor existuje @@ -262,7 +263,7 @@ administrátora. Tento soubor již není dostupný na serveru Účty Přidat účet - Zabezpečené spojení je přesměrováváno nezabezpečenou trasou. + Bezpečné spojení je přesměrováno na nezabezpečenou trasu. Logy Odeslat historii Logy aplikace ownCloud pro Android @@ -277,4 +278,6 @@ administrátora. Soubor již v cílovém adresáři existuje Při pokusu o přesun tohoto souboru či složky nastala chyba pro přesun tohoto souboru + Okamžitá odesílání + Zabezpečení diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml index 15475299a3..771b3ce193 100644 --- a/res/values-da/strings.xml +++ b/res/values-da/strings.xml @@ -261,7 +261,6 @@ Filen er ikke længere tilgængelig på serveren Konti Tilføj konto - Sikker forbindelse videredirigeres gennem en usikker rute. Logge Send historik App-logregistreringer for ownCloud Android @@ -276,4 +275,5 @@ Filen findes allerede i destinationsmappen Der opstod en fejl under forsøg på at flytte denne mappe eller fil til at flytte denne fil + Sikkerhed diff --git a/res/values-de-rCH/strings.xml b/res/values-de-rCH/strings.xml index a6cb3c2710..7a5b9e9148 100644 --- a/res/values-de-rCH/strings.xml +++ b/res/values-de-rCH/strings.xml @@ -198,4 +198,5 @@ Konten Auswählen + Sicherheit diff --git a/res/values-de-rDE/strings.xml b/res/values-de-rDE/strings.xml index b11328715b..70bd98fa03 100644 --- a/res/values-de-rDE/strings.xml +++ b/res/values-de-rDE/strings.xml @@ -238,6 +238,7 @@ Bildvorschau Dieses Bild kann nicht angezeigt werden %1$s konnte nicht in den lokalen %2$s Ordner kopiert werden + Pfad hochladen Entschuldigung, Freigaben sind auf Ihrem Server nicht aktiviert. Bitte kontaktieren Sie Ihren ⇥⇥Administrator. Teilen nicht möglich. Prüfen Sie, dass die Datei existiert @@ -278,4 +279,6 @@ Die Datei ist bereits im Zielordner vorhanden Es ist ein Fehler beim Verschieben dieser Datei oder dieses Ordners aufgetreten. um diese Datei zu verschieben + Sofortiges Hochladen + Sicherheit diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index c859bb6e96..028b6bc6a0 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -238,6 +238,7 @@ Bildvorschau Dieses Bild kann nicht angezeigt werden %1$s konnte nicht in den lokalen %2$s Ordner kopiert werden + Pfad hochladen Entschuldigung, Freigaben sind auf Deinem Server nicht aktiviert. Bitte kontaktiere Deinen ⇥⇥Administrator. Teilen nicht möglich. Prüfe, dass die Datei existiert @@ -263,7 +264,7 @@ Diese Datei steht auf dem Server nicht mehr zur Verfügung Konten Konto hinzufügen - Die gesicherte Verbindung wird durch eine ungesicherte Route geleitet. + Die gesicherte Verbindung wird auf eine unsichere Route weitergeleitet. Protokolle Verlauf senden Protokolle der ownCloud-Android-App @@ -278,4 +279,6 @@ Die Datei ist bereits im Zielordner vorhanden Es ist ein Fehler beim Verschieben der Datei oder des Ordners aufgetreten. um diese Datei zu verschieben + Sofortiges Hochladen + Sicherheit diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index 8cbfca9a94..2fb159a6f0 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -260,7 +260,6 @@ Αυτό το αρχείο δεν είναι πια διαθέσιμο στο διακομιστή Λογαριασμοί Προσθήκη λογαριασμού - Ασφαλής σύνδεση ανακατευθύνεται μέσω μιας μη ασφαλούς διαδρομής. Αρχεία καταγραφών Αποστολή ιστορικού Φόρτωση δεδομένων.... @@ -270,4 +269,5 @@ Επιλέξτε Το αρχείο υπάρχει ήδη στο φάκελο προορισμού για μετακίνηση αυτού του αρχείου + Ασφάλεια diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml index fd812c6484..320d1e6700 100644 --- a/res/values-en-rGB/strings.xml +++ b/res/values-en-rGB/strings.xml @@ -238,6 +238,7 @@ Image preview This image cannot be shown %1$s could not be copied to %2$s local folder + Upload Path Sorry, sharing is not enabled on your server. Please contact your administrator. Unable to share. Please check whether the file exists @@ -263,7 +264,7 @@ The file is no longer available on the server Accounts Add account - Secure connection is redirected through an unsecured route. + Secure connection is redirected to an unsecured route. Logs Send History ownCloud Android app logs @@ -278,4 +279,6 @@ The file exists already in the destination folder An error occurred whilst trying to move this file or folder to move this file + Instant Uploads + Security diff --git a/res/values-eo/strings.xml b/res/values-eo/strings.xml index 065e59aba4..2b4413ced2 100644 --- a/res/values-eo/strings.xml +++ b/res/values-eo/strings.xml @@ -186,4 +186,5 @@ Aŭtentiĝo nepras Malĝusta pasvorto Elekti + Sekuro diff --git a/res/values-es-rAR/strings.xml b/res/values-es-rAR/strings.xml index 09fdb53b76..780a604341 100644 --- a/res/values-es-rAR/strings.xml +++ b/res/values-es-rAR/strings.xml @@ -241,4 +241,5 @@ Autentificación requerida Clave incorrecta Elegir + Seguridad diff --git a/res/values-es-rMX/strings.xml b/res/values-es-rMX/strings.xml index 3772ff7a0c..ef91bc63fa 100644 --- a/res/values-es-rMX/strings.xml +++ b/res/values-es-rMX/strings.xml @@ -215,4 +215,5 @@ Cuentas Contraseña incorrecta Seleccionar + Seguridad diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 02508b6a65..7f661730df 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -101,7 +101,7 @@ Se encontraron conflictos Falló la sincronización de contenidos de %1$d ficheros Fallos en la sincronización de contenidos - Los contenidos de %1$d ficheros no se han sincronizado (%2$d conflictos) + Los contenidos de %1$d archivos no pudieron sincronizarse (%2$d conflictos) Algunos archivos locales se han perdido %1$d archivos en la carpeta %2$s no pudieron ser copiados a A partir de la versión 1.3.16, los ficheros subidos desde este dispositivo se copian en la carpeta local %1$s para evitar la pérdida de datos cuando se sincroniza un único archivo con varias cuentas.\n\nDebido a este cambio, todos los ficheros subidos con versiones anteriores de esta aplicación fueron copiados a la carpeta %2$s. Sin embargo, un error impidió que se completara esta operación durante la sincronización de la cuenta. Puede dejar los archivos tal y como están y eliminar el enlace a %3$s o mover los archivos a la carpeta %1$s y mantener el enlace a %4$s.\n\nDebajo se muestran los archivos locales y los archivos remotos en %5$s a los que fueron enlazados. @@ -150,12 +150,12 @@ Ya existe una cuenta en este dispositivo con los mismos datos de Usuario y Servidor El usuario introducido no concuerda con el usuario de esta cuenta Ocurrió un error desconocido - No se pudo encontrar la dirección + Error: no se pudo encontrar el host Instancia de servidor no encontrada El servidor ha tardado demasiado en responder URL no válida Falló la inicialización SSL - No fue posible verificar la identidad del servidor SLL + No fue posible verificar la identidad del servidor SSL No se reconoce la versión del servidor No se ha podido establecer la conexión Conexión segura establecida @@ -200,7 +200,7 @@ 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 demasiado reciente + - El certificado del servidor es de una fecha que aún no llega - La URL no coincide con el nombre de dominio del certificado ¿Confías de todas formas en este certificado? El certificado no pudo ser guardado @@ -237,7 +237,8 @@ No subir Previsualización de imagen No se puede mostrar la imagen - %1$s no pudo ser copiado a la carpeta local %2$s + %1$s se pudo copiar a la carpeta local %2$s + Ruta de subida La función Compartir no está activada en su servidor. Contacte a su administrador. No se puede compartir. Revise si el archivo existe @@ -257,14 +258,14 @@ para renombrar este archivo para eliminar este archivo para compartir este archivo - para ya no compartir este archivo + para dejar de compartir este archivo para crear el archivo para subir archivos a esta carpeta Este archivo ya no se encuentra en el servidor Cuentas Agregar cuenta - La conexión segura está siendo redirigida por una ruta insegura. - Trazas + La conexión segura está siendo desviada por una ruta insegura. + Logs Enviar historial Logs de las apps ownCloud Android Cargando datos... @@ -274,8 +275,10 @@ Aquí no hay nada. ¡Puede agregar una carpeta! Seleccionar No se puede mover. Revise si el archivo existe - No se puede mover una carpeta dentro de una de sus descendientes. + No se puede mover una carpeta dentro de una de SUS subcarpetas. El archivo ya existe en la carpeta de destino Hubo un error al tratar de mover este archivo o carpeta para mover este archivo + Subidas instantáneas + Seguridad diff --git a/res/values-et-rEE/strings.xml b/res/values-et-rEE/strings.xml index 6604a71350..daba47f4a4 100644 --- a/res/values-et-rEE/strings.xml +++ b/res/values-et-rEE/strings.xml @@ -31,6 +31,7 @@ Tagasiside Impressum Proovi oma nutitelefonil rakendust %1$s! + Kutsun sind kasutama oma nutitelefonis rakendust %1$s!\nLaadi see alla siit: %2$s Kontrolli serverit Serveri aadress https://... Kasutajanimi @@ -241,6 +242,7 @@ Allpool on loend kohalikest failidest ning serveris asuvatest failidest %5$s, mi Pildi eelvaade Seda pilti ei saa näidata %1$s ei suudetud kopeerida kohalikku kataloogi %2$s + Üleslaadimise rada Vabandust, server ei toeta jagamist. Palun kontakteeru ⇥⇥administraatoriga. Jagamine ebaõnnestus. Palun kontrolli, kas fail on olemas @@ -266,7 +268,11 @@ Allpool on loend kohalikest failidest ning serveris asuvatest failidest %5$s, mi Fail ei ole serveris enam kättesaadav Kontod Lisa konto - Turvalist ühendust suunatakse läbi turvamata ühenduse. + Turvaline ühendus suunatakse läbi turvamata ühenduse. + Logid + Saada ajalugu + ownCloud Android rakenduse logid + Andmete laadimine... Autentimine on vajalik Vale parool Tõsta ümber @@ -277,4 +283,6 @@ Allpool on loend kohalikest failidest ning serveris asuvatest failidest %5$s, mi See fail on juba sihtkaustas olemas Selle faili või kausta liigutamisel tekkis tõrge selle faili liigutamiseks + Kohesed üleslaadimised + Turvalisus diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index 3edade4326..fec7404dbe 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -258,8 +258,8 @@ Mesedez, baimendu berriz Fitxategia jadanik ez dago eskuragarri zerbitzarian Kontuak Gehitu kontua - Konexio segurua birbideratu da segurua ez den bide batetik. Autentikazioa beharrezkoa Pasahitz okerra Aukeratu + Segurtasuna diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml index 200f5e2fb1..6a22f893e4 100644 --- a/res/values-fa/strings.xml +++ b/res/values-fa/strings.xml @@ -232,4 +232,5 @@ احراز هویت مورد نیاز است رمز عبور اشتباه است انتخاب کردن + امنیت diff --git a/res/values-fi-rFI/strings.xml b/res/values-fi-rFI/strings.xml index 9419f95f3a..6f02e4bcb5 100644 --- a/res/values-fi-rFI/strings.xml +++ b/res/values-fi-rFI/strings.xml @@ -218,6 +218,7 @@ Älä lähetä Kuvan esikatselu Tätä kuvaa ei voi näyttää + Lähetyspolku Jakaminen ei ole käytössä palvelimellasi. Ota yhteys ylläpitäjään. Virhe tiedoston tai kansion jakamista yrittäessä @@ -240,7 +241,6 @@ Tämä tiedosto ei ole enää palvelimella käytettävissä Tilit Lisää tili - Salattu yhteys on ohjattu uudelleen salaamatonta reittiä pitkin. Lokit Lähetä historia ownCloudin Android-sovelluksen lokit @@ -253,4 +253,6 @@ Siirto ei onnistu. Tarkista, ettei tiedostoa ole jo olemassa Tiedosto on jo olemassa kohdekansiossa Tämän tiedoston tai kansion siirtoa yrittäessä tapahtui virhe + Välittömät lähetykset + Tietoturva diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 8bb1a0e886..17da4a9701 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -267,7 +267,6 @@ 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 - Le connexion sécurisée est redirigée vers une route non-sécurisée. Journaux Envoyer l\'historique Journaux de l\'application Android ownCloud @@ -282,4 +281,5 @@ 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 + Sécurité diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml index 65344d2fc7..c169179c0c 100644 --- a/res/values-gl/strings.xml +++ b/res/values-gl/strings.xml @@ -259,9 +259,9 @@ O ficheiro xa non está dispoñíbel no servidor Contas Engadir unha conta - A conexión segura está a ser redirixida a través dunha ruta non segura. Requírese autenticación Contrasinal incorrecto Mover Escoller + Seguranza diff --git a/res/values-he/strings.xml b/res/values-he/strings.xml index cd30af291d..4ae0072df8 100644 --- a/res/values-he/strings.xml +++ b/res/values-he/strings.xml @@ -260,6 +260,6 @@ הקובץ אינו זמין יותר על השרת חשבונות הוספת חשבון - חיבור מאובטח מנותב דרך נתיב לא מאובטח בחירה + אבטחה diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml index 60b52f0540..b8dcfc5b1a 100644 --- a/res/values-hr/strings.xml +++ b/res/values-hr/strings.xml @@ -36,4 +36,5 @@ Potrebna autentikacija Pogrešna lozinka Izaberi + Sigurnost diff --git a/res/values-hu-rHU/strings.xml b/res/values-hu-rHU/strings.xml index a0b16a04b3..f590006047 100644 --- a/res/values-hu-rHU/strings.xml +++ b/res/values-hu-rHU/strings.xml @@ -242,4 +242,5 @@ Felhasználóazonosítás szükséges Hibás jelszó Válasszon + Biztonság diff --git a/res/values-id/strings.xml b/res/values-id/strings.xml index f7cfeaa76c..be93270a06 100644 --- a/res/values-id/strings.xml +++ b/res/values-id/strings.xml @@ -6,17 +6,21 @@ Unggah Konten dari apl lain Berkas - Bukan dengan + Buka dengan Folder baru - pengaturan + Pengaturan Rincian Kirim - umum + Umum Lainnya Akun Kelola Akun PIN Apl Lindungi klien Anda + Unggah gambar cepat + Unggah gambar yang diambil kamera dengan cepat + Unggah video cepat + Unggah video yang direkam kamera dengan cepat Aktifkan Pencatatan Ini digunakan untuk mencatat masalah Riwayat Catatan @@ -25,8 +29,9 @@ Bantuan Rekomendasikan ke teman Umpan balik - Imprint - Coba %1$s pada smartphone Anda! + Jejak + Cobalah %1$s pada ponsel cerdas Anda! + Saya ingin mengajak Anda untuk menggunakan %1$s di ponsel cerdas Anda!\nUnduh gratis disini: %2$s Periksa Server Alamat server https://… Nama Pengguna @@ -35,6 +40,7 @@ Berkas Sambungkan Unggah + Pilih folder unggah: Tidak ada akun yang ditemukan Belum ada akun %1$s pada perangkat Anda. Silahkan buat akun terlebih dahulu. Pengaturan @@ -44,6 +50,8 @@ %1$s tidak diizinkan mengakses konten berbagi Mengunggah Tidak ada apa-apa di sini. Unggah sesuatu! + Memuat... + Tidak ada satupun berkas dalam folder ini. Sentuh pada berkas untuk menampilkan informasi tambahan Ukuran: Tipe: @@ -53,6 +61,7 @@ Segarkan berkas Berkas diubah namanya menjadi %1$s saat pengunggahan Bagikan tautan + Batal bagikan tautan Ya Tidak Oke @@ -82,19 +91,26 @@ Gagal Mengunduh Mengunduh %1$s tidak selesai Belum diunduh + Gagal mengunduh, Anda perlu masuk kembali Pilih akun Sinkronisasi gagal + Sinkronisasi gagal, Anda perlu masuk kembali Sinkronisasi %1$s tidak selesai Sandi salah untuk %1$s Konflik ditemukan + %1$d berkas kept-in-sync tidak dapat disinkronkan + Berkas kept-in-sync gagal Konten berkas %1$d tidak dapat disinkronasikan (%2$d konflik) Beberapa berkas lokal terlupakan + %1$d berkas diluar folder %2$s tidak dapat disalin kedalamnya + Sejak versi 1.3.16, berkas-berkas yang diunggah dari piranti ini akan disalin kedalam folder %1$s lokal untuk mencagah kehilangan data ketika berkas tunggal disinkronkan dengan akun lebih dari satu.\n\nAkibat perubahan ini, semua berkas yang diunggah di versi aplikasi sebelumnya disalin kedalam folder %2$s. Namun, sebuah kesalahan mencegah penyelesaian operasi ini saat sinkronisasi berlangsung akun. Anda boleh meninggalkan berkas seperti ini dan menghapus tautan ke %3$s atau memindahkan berkas kedalam folder %1$s dan membiarkan tautan ke %4$s.\n\nYang tampak dibawah adalah berkas lokal, dan berkas remote didalam %5$s yang dihubungkan dengannya. Folder %1$s tidak ada lagi Pindahkan semua Semua berkas sudah dipindahkan Beberapa berkas tidak dapat dipindahkan Lokal: %1$s Jauh: %1$s + Ruang tidak cukup untuk menyalin berkas terpilih kedalam folder %1$s. Apakah Anda ingin memindahkannya saja? Silakan masukkan PIN Apl Masukkan PIN Apl PIN akan selalu diminta setiap kali apl dijalankan @@ -107,6 +123,7 @@ Pemutar musik %1$s %1$s (dimainkan) %1$s (sedang dimuat) + %1$s pemutaran selesai Tidak ditemukan berkas media Tidak ada akun yang diberikan Brkas tidak didalam akun yang sah @@ -122,6 +139,7 @@ Tombol mundur Tombol main dan jeda Tombol maju + Mendapatkan otorisasi... Mencoba untuk masuk... Tidak ada koneksi internet Sambungan aman tidak tersedia @@ -150,9 +168,12 @@ Menyambungkan ke server otentikasi... Server tidak mendukung medote otentikasi ini %1$s tidak mendukung banyak akun + Tidak dapat mengotentikasi pada server ini Biarkan berkas tetap terbaru Ubah nama Hapus + Apakah Anda yakin ingin menghapus %1$s? + Apakah Anda yakin ingin menghapus %1$s dan isinya? Lokal saja Konten lokal saja Hapus dari server @@ -164,10 +185,13 @@ Mengubah nama tidak selesai Berkas jauh tidak dapat diperiksa Isi berkas sudah diselaraskan + Folder tidak dapat dibuat Karakter yang dilarang: / \\ < > : \" | ? * - Tunggu sejenak + Nama berkas tidak boleh kosong + Tunggu sebentar Masalah tidak terduga, silahkan pilih berkas dari apl yang berbeda Tidak ada berkas yang terpilih + Kirim taukan ke Masuk dengan oAuth2 Menyambungkan ke server oAuth2... Identitas situs tidak dapat diverfikasi @@ -192,6 +216,8 @@ Untuk: Tanda tangan: Algoritma: + Sertifikat tidak dapat ditampilkan. + - Tidak ada informasi tantang terror Ini adalah placeholder placeholder.txt Gambar PNG @@ -199,6 +225,7 @@ 18/05/2012 12:23 PM 12:23:45 Hanya unggah gambar via WiFi + Hanya unggah video via WiFi /UnggahInstan Perbarui benturan Berkas jauh %s tidak sinkron dengan berkas lokal. Melanjutkan akan menggantikan konten berkas di server. @@ -206,10 +233,13 @@ Timpa Jangan mengunggah Pratilik gambar + Gambar ini tidak dapat ditampilkan Kirim + Disalin ke papan klip Akun Diperlukan otentikasi Sandi salah Pilih + Keamanan diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index d450760262..42093f7f31 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -263,7 +263,6 @@ Il file non è più disponibile sul server Account Aggiungi account - La connessione sicura è rediretta attraverso un percorso non sicuro. Registri Invia cronologia Registri applicazione ownCloud Android @@ -278,4 +277,5 @@ Il file esiste già nella cartella di destinazione Si è verificato un errore durante il tentativo di spostare il file o la cartella per spostare questo file + Protezione diff --git a/res/values-ja-rJP/strings.xml b/res/values-ja-rJP/strings.xml index 996d16c313..b6d8ffb4ad 100644 --- a/res/values-ja-rJP/strings.xml +++ b/res/values-ja-rJP/strings.xml @@ -264,7 +264,6 @@ ファイルはサーバー上で利用できません アカウント アカウントを追加 - 暗号化されていない接続を経て、暗号化接続へリダイレクトされました。 ログ ログを送信 ownCloud Android アプリログ @@ -279,4 +278,5 @@ そのファイルは、宛先フォルダに既に存在しています。 このファイルまたはフォルダーを移動する際にエラーが発生しました このファイルを移動 + セキュリティ diff --git a/res/values-ka-rGE/strings.xml b/res/values-ka-rGE/strings.xml index b23ca4735b..43b1a456c2 100644 --- a/res/values-ka-rGE/strings.xml +++ b/res/values-ka-rGE/strings.xml @@ -149,4 +149,5 @@ ანგარიში არჩევა + უსაფრთხოება diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml index 3c4f3cd3f0..752985c327 100644 --- a/res/values-km/strings.xml +++ b/res/values-km/strings.xml @@ -83,4 +83,5 @@ គណនី ខុស​ពាក្យ​សម្ងាត់ ជ្រើស + សុវត្ថិភាព diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index b8313dbc4d..4a0df83f71 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -217,4 +217,5 @@ 인증 필요함 잘못된 암호 선택 + 보안 diff --git a/res/values-lt-rLT/strings.xml b/res/values-lt-rLT/strings.xml index 5dfaffa470..70bc85d524 100644 --- a/res/values-lt-rLT/strings.xml +++ b/res/values-lt-rLT/strings.xml @@ -234,4 +234,5 @@ Paskyros Neteisingas slaptažodis Pasirinkite + Saugumas diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml index 41a1872f9e..88df8292b8 100644 --- a/res/values-lv/strings.xml +++ b/res/values-lv/strings.xml @@ -143,4 +143,5 @@ Konti Izvēlieties + Drošība diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml index d7a623013d..e7b4906ec4 100644 --- a/res/values-mk/strings.xml +++ b/res/values-mk/strings.xml @@ -122,4 +122,5 @@ Потребна е автентификација Погрешна лозинка Избери + Безбедност diff --git a/res/values-nb-rNO/strings.xml b/res/values-nb-rNO/strings.xml index dc60a35638..55e2bb4cb5 100644 --- a/res/values-nb-rNO/strings.xml +++ b/res/values-nb-rNO/strings.xml @@ -263,7 +263,6 @@ Filen finnes ikke på serveren lenger Kontoer Legg til en konto - Sikker tilkobling videresendes gjennom en usikker rute. Logger Send historikk logger for ownCloud Android app @@ -278,4 +277,5 @@ Filen finnes allerede i målmappen En feil oppstod ved flytting av denne filen eller mappen å flytte denne filen + Sikkerhet diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index 2f12347a4d..f79d9331ae 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -241,6 +241,7 @@ Hieronder staan de lokale bestanden en de externe bestanden in %5$s waar ze naar Afbeelding voorbeeld Deze afbeelding kan niet worden getoond %1$s kon niet worden gekopieerd naar de %2$s lokale map + Upload pad Sorry, delen is niet mogelijk op uw server. Neem contact op met uw beheerder. Kan dit niet delen. Controleer of dit bestand wel bestaat. @@ -266,7 +267,7 @@ Hieronder staan de lokale bestanden en de externe bestanden in %5$s waar ze naar Dit bestand is niet langer beschikbaar op de server Accounts Toevoegen account - De beveiligde verbinding is omgeleid via een onveilige route. + De beveiligde verbinding is omgeleid naar een onveilige route. Logs Verstuur geschiedenis ownCloud Android app logs @@ -281,4 +282,6 @@ Hieronder staan de lokale bestanden en de externe bestanden in %5$s waar ze naar Het bestand bestaat al in de doelmap Er trad een fout op bij uw poging dit bestand of deze map te verplaatsen om dit bestand te verplaatsen + Directe uploads + Beveiliging diff --git a/res/values-nn-rNO/strings.xml b/res/values-nn-rNO/strings.xml index 8c15264f8e..528bff4fcc 100644 --- a/res/values-nn-rNO/strings.xml +++ b/res/values-nn-rNO/strings.xml @@ -129,4 +129,5 @@ Kontoar Feil passord Vel + Tryggleik diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 9fd718a098..8c0a98787d 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -263,7 +263,6 @@ Ten plik nie jest już dostępny na serwerze Konta Dodaj konto - Bezpieczne połączenie jest przekierowywane przez niezabezpieczone trasy. Logi Wyślij historię Ładuję dane... @@ -277,4 +276,5 @@ Plik istnieje już w folderze docelowym Pojawił się błąd podczas próby przeniesienia tego pliku lub folderu aby przenieść ten plik + Bezpieczeństwo diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index 71df1607c1..3ffb84b83f 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -238,6 +238,7 @@ Pré-visualização da imagem Esta imagem não pode ser mostrada %1$s não pôde ser copiado para pasta local %2$s + Enviar Caminho Desculpe, o compartilhamento não está habilitado em seu servidor. Entre em contato com seu ⇥⇥ administrador. Não é possível compartilhar. Por favor verifique se o arquivo existe @@ -263,7 +264,7 @@ Este arquivo não mais está disponível neste servidor Contas Adicionar uma conta - A conexão segura está redirecionada através de uma rota insegura. + Conexão segura esta redirecionada para uma rota não segura. Logs Enviar Histórico Logs do aplicativo ownCloud Android @@ -278,4 +279,6 @@ O arquivo já existe na pasta de destino Ocorreu um erro ao tentar mover este arquivo ou pasta mover este arquivo + Envios Instantâneos + Segurança diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml index dcf877893c..6c4aab17de 100644 --- a/res/values-pt-rPT/strings.xml +++ b/res/values-pt-rPT/strings.xml @@ -31,6 +31,7 @@ Resposta Imprint Experimente %1$s no seu smartphone! + Quero convidar-te a usares %1$s no teu smartphone!\nFaz download aqui: %2$s Verificar Servidor Endereço do servidor https://.. Nome de Utilizador @@ -236,9 +237,11 @@ Pré-Visualização da imagem Esta imagem não pode ser mostrada Não foi possível copiar %1$s para a pasta local %2$s + Caminho de Upload Lamentamos mas não é possível partilhar através do seu servidor. Por favor contacte o seu administrador. Não é possivel partilhar. Por favor verifique se o ficheiro existe Ocorreu um erro enquanto tentava partilhar este ficheiro ou pasta + Não é possível retirar a partilha. Verifique se o ficheiro existe Ocorreu um erro enquanto retirava a partilha deste ficheiro ou pasta Enviar Copiar ligação @@ -259,14 +262,21 @@ O ficheiro não está mais disponível no servidor Contas Adicionar conta - Uma ligação segura foi redireccionada por uma rota insegura. + Ligação segura é redireccionada para um caminho inseguro. + Logs Enviar Histórico + Logs da app ownCloud Android A carregar os dados... Autenticação necessária Password errada Mover Não está aqui nada. Pode adicionar uma pasta! Escolha + Não é possível mover. Verifique se o ficheiro existe + Não é possível mover esta pasta deste modo O ficheiro já existe na pasta de destino + Um erro ocorreu ao tentar mover este ficheiro ou pasta para mover este ficheiro + Uploads Instantâneos + Segurança diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml index c3e20eee68..b3ea72569f 100644 --- a/res/values-ro/strings.xml +++ b/res/values-ro/strings.xml @@ -259,4 +259,5 @@ Conturi Parolă greșită Alege + Securitate diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 49c3e3cc2d..66ad7ad03c 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -263,7 +263,6 @@ Этот файл больше недоступен на сервере Учётные записи Добавить учетную запись - Безопасное соединение перенаправлено через небезопасный маршрут. Журналы История Отправлений Журналы Андроид-приложения ownCloud @@ -278,4 +277,5 @@ Файл уже существует в папке назначения Произошла ошибка при попытке перемещения этого файла или папки переместить этот файл + Безопасность diff --git a/res/values-sk-rSK/strings.xml b/res/values-sk-rSK/strings.xml index 78e0942522..768604358d 100644 --- a/res/values-sk-rSK/strings.xml +++ b/res/values-sk-rSK/strings.xml @@ -259,9 +259,9 @@ Súbor už na serveri nie je dostupný Účty Pridať účet - Zabezpečené spojenie je presmerované nezabezpečenou cestou. Vyžaduje sa overenie Nesprávne heslo Presunúť Vybrať + Zabezpečenie diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index da73b33b7c..dcdf60381c 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -263,7 +263,6 @@ Datoteka na strežniku ni več na voljo. Računi Dodaj račun - Varna povezava je preusmerjena preko ne-varne poti. Dnevnik Pošlji zgodovino Dnevnik programa ownCloud @@ -278,4 +277,5 @@ Datoteka v ciljni mapi že obstaja. Prišlo je do napake med premikanjem datoteke v mapo med premikanjem datoteke + Varnost diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml index def1b9b8e8..adeb3e99c6 100644 --- a/res/values-sq/strings.xml +++ b/res/values-sq/strings.xml @@ -72,4 +72,5 @@ Llogarit Fjalëkalim i gabuar Zgjidh + Siguria diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index 5384da08d8..2ef137369c 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -109,4 +109,5 @@ Налози Одабери + Безбедност diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index be0067470b..c077786f75 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -30,8 +30,8 @@ Rekommendera till en vän Feedback Imprint - Försök %1$s på din smarttelefon! - Jag skulle vilja bjuda in dig till att använda %1$s på din smartphone!\nLadda ner här: %2$s + 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 Serveradress https://... Användarnamn @@ -268,4 +268,5 @@ Välj Gick inte att flytta. Vänligen kontrollera att filen existerar att flytta den här filen + Säkerhet diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml index 72a158915e..e5bfe93dea 100644 --- a/res/values-tr/strings.xml +++ b/res/values-tr/strings.xml @@ -238,6 +238,7 @@ Resim önizleme Bu resim gösterilemiyor %1$s, %2$s yerel klasörüne kopyalanamadı + Yükleme Yolu Üzgünüz, paylaşım sunucunuzda etkin değil. Lütfen yöneticinizle iletişime geçin. Paylaşma başarısız. Lütfen dosyanın mevcut olup olmadığını denetleyin @@ -263,7 +264,7 @@ Bu dosya artık sunucuda mevcut değil Hesaplar Hesap ekle - Güvenli bağlantı, güvenli olmayan bir rotaya yönlendiriliyor. + Güvenli bağlantı, güvenli olmayan bir rotaya yönlendirildi. Günlükler Geçmişi Gönder ownCloud Android uygulama kayıtları @@ -278,4 +279,6 @@ Dosya zaten hedef klasörde mevcut Bu dosya veya klasörü taşımaya çalışılırken bir hata oluştu bu dosyayı taşımak için + Anında Yüklemeler + Güvenlik diff --git a/res/values-ug/strings.xml b/res/values-ug/strings.xml index b126d8972f..e1140f871b 100644 --- a/res/values-ug/strings.xml +++ b/res/values-ug/strings.xml @@ -41,4 +41,5 @@ يوللا ھېساباتلار + بىخەتەرلىك diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index 68185743bc..d927e39fe5 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -1,8 +1,12 @@ + %1$s Android App + версія %1$s + Оновити account Відвантажити Вміст із інших програм Файли + Відкрити за допомогою Нова тека Налаштування Деталі @@ -13,14 +17,30 @@ Управління обліковими записами App програмний PIN Захист Вашог App клієнта + Миттєві зображення + Миттєві зображення з камери + Миттєві відео + Миттєві відео з камери + Ввімкнути журнал + Використовується для реєстрації помилок + Журнал + Тут показані записи журналу + Видалити історію записів Допомога + Порадити товаришу Зворотній зв\'язок Відбиток + Спробуйте %1$s на своєму смартфоні! + Пропоную вам користуватися %1$s на вашому смартфоні!\nЗавантажити можна за посиланням: %2$s + Перевірити сервер + Адреса серверу https://… Ім\'я користувача Пароль + Вперше в %1$s? Файли З\'єднати Відвантажити + Оберіть теку для завантаження: Не знайдено облікового запису На Вашому пристрої відсутні облікові записи %1$s. Будь ласка, спочатку створіть запис. Налаштування @@ -30,14 +50,18 @@ %1$s не може отримати доступ до спільного контенту Завантаження Тут нічого немає. Відвантажте що-небудь! + Завантаження... + В цій теці немає файлів. Натисніть на файлі для відображення додаткової інформації Розмір: Тип: Створено: Змінено: Завантажити + Оновити файл Файл був переіменований в %1$s протягом вивантаження Опублікувати посилання + Видалити посилання Так Ні OK @@ -46,6 +70,7 @@ Відмінити Зберегти & Вихід Помилка + Завантаження... Невідома помилка Про Змінити пароль @@ -59,25 +84,34 @@ %1$s було успішно завантажено Помилка завантаження Завантаження %1$s не може завершитись + Завантажити не вдалося, необхідно повторити вхід Зкачування … %1$d%% Зкачування %2$s Успішно зкачано %1$s успішно завантажено Завантаження не вдалося Завантаження %1$s не вдається завершити + Ще не завантажене + Зберегти не вдалося, необхідно повторити вхід Оберіть обліковий запис Помилка синхронізації + Синхронізація не вдалася, необхідно повторити вхід Синхронізація %1$s не вдалась + Невірний пароль для %1$s Конфліктів знайдено %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$s білше не існує Перемістити все Всі файли були переміщені Деякі файли не можуть бути переміщені Локально: %1$s Віддалено: %1$s + Недостатньо місця для копіювання обраних файлів у теку %1$s. Чи бажаєте ви перемістити їх замість копіювання? Будь ласка, введіть свій програмний PIN Введіть програмний PIN PIN необхідно буде вводити щоразу при запуску цієї програми @@ -87,24 +121,62 @@ Не вірний App програмний PIN App програмний PIN видалено App програмний PIN збережено + %1$s музичний плеєр + %1$s (відтворення) + %1$s (завантаження) + %1$s відтворення завершене + Медіа-файлів не знайдено + Обліковий запис не налаштований + Файл в невірному обліковому записі + Кодек не підтримується + Медіа-файл не читається + Медіа-файл невірно закодований + Вийшов час на спробу відтворення + Неможливо потоком відтворити файл + Медіа-файл неможливо відтворити вбудованим програвачем + Помилка безпеки при відтворені %1$s + Помилка вводу при відтворені %1$s + Несподівана помилка при відтворені %1$s + Перемтка назад + Відтворення або пауза + Перемотка вперед + Виконується вхід... Спроба входу… Відсутнє підключення до мережі Безпечне з\'єднання не доступне. З\'єднання встановлено Перевірка з\'єднання… Не вірні налаштування сервер + Такий обліковий запис вже існує на пристрої + Введений користувач не відповідає обліковому запису Виникла невідома помилка! Не вдалося знайти хост Не знайдено примірник сервер Сервер занадто довго не відповідає Пошкоджений URL Помилка SSL ініціалізації + Неможливо перевірити SSL-сертифікат сервера Не вдалося визначити версію сервер серверу Не вдалося встановити з\'єднання Встановлено захищене з\'єднання + Невірне ім\'я користувача або пароль + Невдала авторизація + Доступ заборонений сервером авторизації + Несподівана відповідь; будь ласка, введіть адресу сервера знову + Час авторизації минув. Будь ласка, увійдіть знову + Будь ласка, введіть пароль + Час сесії минув. Будь ласка, підключіться знов + Підключення до серверу аутентифікації... + Сервер не підтримує обраний метод аутентифікації + %1$s не підтримує одночасно декілька облікових записів + Ваш сервер не повертає коректний ідентифікатор користувача, будь ласка зверніться до адміністратора +⇥ + Аутентифікація на цьому сервері неможлива Оновлювати файл Перейменувати Видалити + Ви дійсно бажаєте видалити %1$s? + Ви дійсно бажаєте видалити %1$s та весь вміст? Лише локально Лише локальний зміст Видалити із серверу @@ -116,9 +188,15 @@ Перейменування не вдалося Неможливо перевірити віддалений файл Зміст файлу вже синхронізовано + Не вдалося створити теку + Заборонені символи: / \\ < > : \" | ? * + Ім\'я файлу не може бути порожнім. Зачекайте хвилинку Несподівані проблеми ; будь ласка, спробуйте використати іншу програму для вибору файлу Не обрано файл + Надіслати посилання... + Увійти через oAuth2 + Підключення до серверу oAuth2... Не вдалося перевірити ідентифікацію сайта - Не довірений сертифікат серверу - Сертифікат серверу втратив чинність @@ -141,17 +219,65 @@ До: Підпис: Алгоритм: + Не вдалося показати сертифікат. + - Інформація про помилку відсутня Це заповнювач + placeholder.txt + PNG зображення + 389 КБ + 2012/05/18 12:23 PM + 12:23:45 Завантажувати зображення тільки через WiFi + Завантажувати відео тільки через WiFi /InstantUpload Конфлікт оновлення Віддалений файл %s не синхронізовано з локальним. Продовження процедури замінить вміст файлу на сервері. Залишити обидва Замінити Не завантажувати + Попередній перегляд зображення + Не вдалося показати зображення + %1$s неможливо скопіювати до %2$s + Завантажити шлях + На жаль, обмін не включений на вашому сервері. Будь ласка, зв\'яжіться з вашим адмінистратором. + Неможливо поділитися. Будь ласка, перевірте, чи існує файл + Виникла помилка при спробі поділитися файлом або текою + Неможливо заборонити доступ. Будь ласка, перевірте, чи існує файл + Виникла помилка при спробі заборонити доступ до файлу або теки Надіслати + Копіювати посилання Скопійовано в буфер обміну + Критична помилка: виконання операції неможливе + Виникла помилка при підключені до сервера. + Під час очікування на сервер виникла помилка, операцію неможливо завершити + Під час очікування на сервер виникла помилка, операцію неможливо завершити + Неможливо завершити операцію, сервер недоступний + У вас немає повноважень %s + на перейменування цього файла + на видалення цього файла + для надання доступу до файла + для закриття доступу до файла + для створення файла + для завантаження в цю теку + Файл більше не доступний на сервері Облікові записи + Додати обліковий запис + Безпечне підключення перенаправляється через незабезпечений маршрут. + Журнали + Надіслати історію + Журнали Android-додатка ownCloud + Завантаження даних... + Потрібна аутентифікація + Невірний пароль + Перемістити + Тут нічого немає. Ви можете додати теку! Обрати + Неможливо перемістити. Будь ласка, перевірте, чи існує файл + Неможливо перемістити теку до теки-нащадка + Файл вже існує в теці призначення + Виникла помилка при спробі перемістити файл або теку + перемістити цей файл + Миттєво завантаження + Безпека diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index c744b68dfe..79255f8dee 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -263,7 +263,6 @@ 该文件在服务器上不可用 账号 添加账号 - 安全连接是通过一个非安全路由定向的。 日志 发送历史 ownCloud安卓客户端日志 @@ -278,4 +277,5 @@ 该文件已经存在在目标文件夹 尝试移动该文件或文件夹时发生错误 移动该文件 + 安全 diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml index aca365e0f2..91041380b7 100644 --- a/res/values-zh-rTW/strings.xml +++ b/res/values-zh-rTW/strings.xml @@ -31,6 +31,7 @@ 反饋 法律聲明 在您的手機中試用%1$s! + 我想邀請您在您的手機上使用 %1$s ! 可以由這兒下載: %2$s 檢查伺服器 伺服器位址 https://... 使用者名稱 @@ -49,6 +50,8 @@ %1$s 並沒有被允許存取分享的內容 上傳中 這裡還沒有東西,上傳一些吧! + 載入中… + 這個目錄中沒有任何檔案. 在檔案上輕觸來顯示更多資訊。 容量: 類型: @@ -92,6 +95,7 @@ 下傳失敗, 您需要重新登入 選擇帳號 同步失敗 + 同步作業失敗, 您需要重新登入 同步 %1$s 未完成 無效的密碼 %1$s 出現衝突 @@ -100,12 +104,14 @@ %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$s 不存在 移動全部 所有文件已被移動 部份文件無法被移動 本地: %1$s 遠端: %1$s + 無足夠的空間可以複製檔案到 %1$s 目錄. 是否使用移動的方式來處理? 請輸入您的 App 密碼 輸入您的 App 密碼 這個密碼在你每次啟動這程式時都會被要求輸入 @@ -232,9 +238,12 @@ 圖片預覽 無法顯示圖片 %1$s 無法被複製到本地目錄 %2$s + 上傳目錄 很抱歉, 您的伺服器並未開啟分享的功能. 請聯絡您的 伺服器管理員. + 無法分享這個檔案或目錄. 請檢查它們是否存在 在分享檔案或目錄時發生了錯誤 + 無法取消分享這個檔案或目錄. 請檢查它們是否存在 在取消分享檔案或目錄時發生了錯誤 寄出 複製連結 @@ -245,8 +254,31 @@ 在等待伺服器回應時發生了錯誤, 這個操作將無法被完成 這個操作無法完成, 無法使用伺服器 + 您沒有權限 %s + 重新命名檔案 + 刪除檔案 + 分享檔案 + 取消分享檔案 + 建立檔案 + 上傳這個目錄 + 這個檔案已經不存在於伺服器中 帳號 + 新增帳號 + 安全連線被轉向到一個非安全的連線 + 紀錄 + 傳送歷史記錄 + ownCloud Android 應用程式記錄 + 資料載入中... 必須驗證 密碼錯誤 + 移動 + 找不到任何檔案. 你可以新增一個目錄! 選擇 + 無法搬移. 請檢查該檔案是否存在 + 把一個目錄搬移到其底下的子目錄是不可能的 + 這個檔案已經存在於目的目錄中 + 在移動檔案或目錄時發生了錯誤 + 移動這個檔案 + 即時上傳 + 安全性 diff --git a/res/values/strings.xml b/res/values/strings.xml index 4c7cf34d10..389db1da41 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -251,6 +251,7 @@ This image cannot be shown %1$s could not be copied to %2$s local folder + Upload Path Sorry, sharing is not enabled on your server. Please contact your administrator. @@ -283,7 +284,7 @@ Accounts Add account - Secure connection is redirected through an unsecured route. + Secure connection is redirected to an unsecured route. Logs Send History @@ -302,4 +303,7 @@ An error occurred while trying to move this file or folder to move this file + Instant Uploads + Security + diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 945e853cc7..3b8b3e81b5 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -21,35 +21,41 @@ - - - + + - - - - - + + + + + + + + + @@ -65,4 +71,4 @@ - \ No newline at end of file + diff --git a/setup_env.sh b/setup_env.sh index e60475288c..ae4214238b 100755 --- a/setup_env.sh +++ b/setup_env.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash -e git submodule init git submodule update diff --git a/src/com/owncloud/android/MainApp.java b/src/com/owncloud/android/MainApp.java index 9a47bd78d0..e04239df87 100644 --- a/src/com/owncloud/android/MainApp.java +++ b/src/com/owncloud/android/MainApp.java @@ -19,6 +19,7 @@ package com.owncloud.android; import android.app.Application; import android.content.Context; +import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; import com.owncloud.android.lib.common.OwnCloudClientManagerFactory.Policy; import com.owncloud.android.lib.common.utils.Log_OC; @@ -55,6 +56,9 @@ public class MainApp extends Application { OwnCloudClientManagerFactory.setDefaultPolicy(Policy.ALWAYS_NEW_CLIENT); } + // initialise thumbnails cache on background thread + new ThumbnailsCacheManager.InitDiskCacheTask().execute(); + if (BuildConfig.DEBUG) { String dataFolder = getDataFolder(); diff --git a/src/com/owncloud/android/authentication/AuthenticatorActivity.java b/src/com/owncloud/android/authentication/AuthenticatorActivity.java index ee38c4b50e..0f7892ee18 100644 --- a/src/com/owncloud/android/authentication/AuthenticatorActivity.java +++ b/src/com/owncloud/android/authentication/AuthenticatorActivity.java @@ -247,13 +247,17 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { if (mAccount != null) { boolean oAuthRequired = (mAccountMgr.getUserData(mAccount, Constants.KEY_SUPPORTS_OAUTH2) != null); - boolean samlWebSsoRequired = - (mAccountMgr.getUserData(mAccount, Constants.KEY_SUPPORTS_SAML_WEB_SSO) != null); + boolean samlWebSsoRequired = ( + mAccountMgr.getUserData( + mAccount, Constants.KEY_SUPPORTS_SAML_WEB_SSO + ) != null + ); mAuthTokenType = chooseAuthTokenType(oAuthRequired, samlWebSsoRequired); } else { boolean oAuthSupported = AUTH_ON.equals(getString(R.string.auth_method_oauth2)); - boolean samlWebSsoSupported = AUTH_ON.equals(getString(R.string.auth_method_saml_web_sso)); + boolean samlWebSsoSupported = + AUTH_ON.equals(getString(R.string.auth_method_saml_web_sso)); mAuthTokenType = chooseAuthTokenType(oAuthSupported, samlWebSsoSupported); } } @@ -322,7 +326,8 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { if (savedInstanceState == null) { if (mAccount != null) { mServerInfo.mBaseUrl = mAccountMgr.getUserData(mAccount, Constants.KEY_OC_BASE_URL); - mServerInfo.mIsSslConn = mServerInfo.mBaseUrl.startsWith("https://"); // TODO do this in a setter for mBaseUrl + // TODO do next in a setter for mBaseUrl + mServerInfo.mIsSslConn = mServerInfo.mBaseUrl.startsWith("https://"); String ocVersion = mAccountMgr.getUserData(mAccount, Constants.KEY_OC_VERSION); if (ocVersion != null) { mServerInfo.mVersion = new OwnCloudVersion(ocVersion); @@ -407,8 +412,12 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { @Override public boolean onTouch(View view, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { - if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType()).equals(mAuthTokenType) && - mHostUrlInput.hasFocus()) { + if ( + AccountTypeUtils.getAuthTokenTypeSamlSessionCookie( + MainApp.getAccountType() + ).equals(mAuthTokenType) && + mHostUrlInput.hasFocus() + ) { checkOcServer(); } } @@ -536,8 +545,9 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { /** * Saves relevant state before {@link #onPause()} * - * Do NOT save {@link #mNewCapturedUriFromOAuth2Redirection}; it keeps a temporal flag, intended to defer the - * processing of the redirection caught in {@link #onNewIntent(Intent)} until {@link #onResume()} + * Do NOT save {@link #mNewCapturedUriFromOAuth2Redirection}; it keeps a temporal flag, + * intended to defer the processing of the redirection caught in + * {@link #onNewIntent(Intent)} until {@link #onResume()} * * See {@link #loadSavedInstanceState(Bundle)} */ @@ -576,11 +586,11 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { /** - * The redirection triggered by the OAuth authentication server as response to the GET AUTHORIZATION request - * is caught here. + * The redirection triggered by the OAuth authentication server as response to the + * GET AUTHORIZATION request is caught here. * - * To make this possible, this activity needs to be qualified with android:launchMode = "singleTask" in the - * AndroidManifest.xml file. + * To make this possible, this activity needs to be qualified with android:launchMode = + * "singleTask" in the AndroidManifest.xml file. */ @Override protected void onNewIntent (Intent intent) { @@ -593,8 +603,8 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { /** - * The redirection triggered by the OAuth authentication server as response to the GET AUTHORIZATION, and - * deferred in {@link #onNewIntent(Intent)}, is processed here. + * The redirection triggered by the OAuth authentication server as response to the + * GET AUTHORIZATION, and deferred in {@link #onNewIntent(Intent)}, is processed here. */ @Override protected void onResume() { @@ -737,7 +747,10 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { Intent getServerInfoIntent = new Intent(); getServerInfoIntent.setAction(OperationsService.ACTION_GET_SERVER_INFO); - getServerInfoIntent.putExtra(OperationsService.EXTRA_SERVER_URL, uri); + getServerInfoIntent.putExtra( + OperationsService.EXTRA_SERVER_URL, + normalizeUrlSuffix(uri) + ); if (mOperationsServiceBinder != null) { mWaitingForOpId = mOperationsServiceBinder.newOperation(getServerInfoIntent); } else { @@ -781,7 +794,8 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { } private boolean isPasswordVisible() { - return ((mPasswordInput.getInputType() & InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); + return ((mPasswordInput.getInputType() & InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) == + InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); } private void hidePasswordButton() { @@ -789,12 +803,16 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { } private void showPassword() { - mPasswordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); + mPasswordInput.setInputType( + InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + ); showViewPasswordButton(); } private void hidePassword() { - mPasswordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + mPasswordInput.setInputType( + InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD + ); showViewPasswordButton(); } @@ -826,9 +844,13 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { return; } - if (AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType()).equals(mAuthTokenType)) { + if (AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType()). + equals(mAuthTokenType)) { + startOauthorization(); - } else if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType()).equals(mAuthTokenType)) { + } else if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType()). + equals(mAuthTokenType)) { + startSamlBasedFederatedSingleSignOnAuthorization(); } else { checkBasicAuthorization(); @@ -883,10 +905,18 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { // GET AUTHORIZATION request Uri uri = Uri.parse(mOAuthAuthEndpointText.getText().toString().trim()); Uri.Builder uriBuilder = uri.buildUpon(); - uriBuilder.appendQueryParameter(OAuth2Constants.KEY_RESPONSE_TYPE, getString(R.string.oauth2_response_type)); - uriBuilder.appendQueryParameter(OAuth2Constants.KEY_REDIRECT_URI, getString(R.string.oauth2_redirect_uri)); - uriBuilder.appendQueryParameter(OAuth2Constants.KEY_CLIENT_ID, getString(R.string.oauth2_client_id)); - uriBuilder.appendQueryParameter(OAuth2Constants.KEY_SCOPE, getString(R.string.oauth2_scope)); + uriBuilder.appendQueryParameter( + OAuth2Constants.KEY_RESPONSE_TYPE, getString(R.string.oauth2_response_type) + ); + uriBuilder.appendQueryParameter( + OAuth2Constants.KEY_REDIRECT_URI, getString(R.string.oauth2_redirect_uri) + ); + uriBuilder.appendQueryParameter( + OAuth2Constants.KEY_CLIENT_ID, getString(R.string.oauth2_client_id) + ); + uriBuilder.appendQueryParameter( + OAuth2Constants.KEY_SCOPE, getString(R.string.oauth2_scope) + ); uri = uriBuilder.build(); Log_OC.d(TAG, "Starting browser to view " + uri.toString()); Intent i = new Intent(Intent.ACTION_VIEW, uri); @@ -931,7 +961,8 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { } else if (operation instanceof ExistenceCheckRemoteOperation) { //Log_OC.wtf(TAG, "received detection response through callback" ); - if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType()).equals(mAuthTokenType)) { + if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType()). + equals(mAuthTokenType)) { onSamlBasedFederatedSingleSignOnAuthorizationStart(result); } else { @@ -1084,16 +1115,20 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { url = "http://" + url; } } - - url = trimUrlWebdav(url); - - if (url.endsWith("/")) { - url = url.substring(0, url.length() - 1); - } - + + url = normalizeUrlSuffix(url); } return (url != null ? url : ""); } + + + private String normalizeUrlSuffix(String url) { + if (url.endsWith("/")) { + url = url.substring(0, url.length() - 1); + } + url = trimUrlWebdav(url); + return url; + } // TODO remove, if possible @@ -1303,7 +1338,6 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { @SuppressWarnings("unchecked") Map tokens = (Map)(result.getData().get(0)); mAuthToken = tokens.get(OAuth2Constants.KEY_ACCESS_TOKEN); - //mAuthToken = ((OAuth2GetAccessToken)operation).getResultTokenMap().get(OAuth2Constants.KEY_ACCESS_TOKEN); Log_OC.d(TAG, "Got ACCESS TOKEN: " + mAuthToken); accessRootFolderRemoteOperation("", ""); @@ -1362,7 +1396,7 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { showRefreshButton(true); mOkButton.setEnabled(false); - // very special case (TODO: move to a common place for all the remote operations) (dangerous here?) + // very special case (TODO: move to a common place for all the remote operations) if (result.getCode() == ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED) { showUntrustedCertDialog(result); } @@ -1378,23 +1412,27 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { /** - * Sets the proper response to get that the Account Authenticator that started this activity saves - * a new authorization token for mAccount. + * Sets the proper response to get that the Account Authenticator that started this activity + * saves a new authorization token for mAccount. */ private void updateToken() { Bundle response = new Bundle(); response.putString(AccountManager.KEY_ACCOUNT_NAME, mAccount.name); response.putString(AccountManager.KEY_ACCOUNT_TYPE, mAccount.type); - if (AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType()).equals(mAuthTokenType)) { + if (AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType()). + equals(mAuthTokenType)) { response.putString(AccountManager.KEY_AUTHTOKEN, mAuthToken); - // the next line is necessary; by now, notifications are calling directly to the AuthenticatorActivity to update, without AccountManager intervention + // the next line is necessary, notifications are calling directly to the + // AuthenticatorActivity to update, without AccountManager intervention mAccountMgr.setAuthToken(mAccount, mAuthTokenType, mAuthToken); - } else if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType()).equals(mAuthTokenType)) { + } else if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType()). + equals(mAuthTokenType)) { response.putString(AccountManager.KEY_AUTHTOKEN, mAuthToken); - // the next line is necessary; by now, notifications are calling directly to the AuthenticatorActivity to update, without AccountManager intervention + // the next line is necessary; by now, notifications are calling directly to the + // AuthenticatorActivity to update, without AccountManager intervention mAccountMgr.setAuthToken(mAccount, mAuthTokenType, mAuthToken); } else { @@ -1415,8 +1453,10 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { */ private boolean createAccount() { /// create and save new ownCloud account - boolean isOAuth = AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType()).equals(mAuthTokenType); - boolean isSaml = AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType()).equals(mAuthTokenType); + boolean isOAuth = AccountTypeUtils. + getAuthTokenTypeAccessToken(MainApp.getAccountType()).equals(mAuthTokenType); + boolean isSaml = AccountTypeUtils. + getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType()).equals(mAuthTokenType); Uri uri = Uri.parse(mServerInfo.mBaseUrl); String username = mUsernameInput.getText().toString().trim(); @@ -1438,9 +1478,12 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { mAccount = newAccount; if (isOAuth || isSaml) { - mAccountMgr.addAccountExplicitly(mAccount, "", null); // with external authorizations, the password is never input in the app + // with external authorizations, the password is never input in the app + mAccountMgr.addAccountExplicitly(mAccount, "", null); } else { - mAccountMgr.addAccountExplicitly(mAccount, mPasswordInput.getText().toString(), null); + mAccountMgr.addAccountExplicitly( + mAccount, mPasswordInput.getText().toString(), null + ); } /// add the new account as default in preferences, if there is none already @@ -1453,7 +1496,8 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { } /// prepare result to return to the Authenticator - // TODO check again what the Authenticator makes with it; probably has the same effect as addAccountExplicitly, but it's not well done + // TODO check again what the Authenticator makes with it; probably has the same + // effect as addAccountExplicitly, but it's not well done final Intent intent = new Intent(); intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, MainApp.getAccountType()); intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, mAccount.name); @@ -1463,9 +1507,14 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { if (isOAuth || isSaml) { mAccountMgr.setAuthToken(mAccount, mAuthTokenType, mAuthToken); } - /// add user data to the new account; TODO probably can be done in the last parameter addAccountExplicitly, or in KEY_USERDATA - mAccountMgr.setUserData(mAccount, Constants.KEY_OC_VERSION, mServerInfo.mVersion.getVersion()); - mAccountMgr.setUserData(mAccount, Constants.KEY_OC_BASE_URL, mServerInfo.mBaseUrl); + /// add user data to the new account; TODO probably can be done in the last parameter + // addAccountExplicitly, or in KEY_USERDATA + mAccountMgr.setUserData( + mAccount, Constants.KEY_OC_VERSION, mServerInfo.mVersion.getVersion() + ); + mAccountMgr.setUserData( + mAccount, Constants.KEY_OC_BASE_URL, mServerInfo.mBaseUrl + ); if (isSaml) { mAccountMgr.setUserData(mAccount, Constants.KEY_SUPPORTS_SAML_WEB_SSO, "TRUE"); @@ -1487,7 +1536,9 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { * @param view 'Account register' button */ public void onRegisterClick(View view) { - Intent register = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.welcome_link_url))); + Intent register = new Intent( + Intent.ACTION_VIEW, Uri.parse(getString(R.string.welcome_link_url)) + ); setResult(RESULT_CANCELED); startActivity(register); } @@ -1587,18 +1638,21 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { /** * Called when the 'action' button in an IME is pressed ('enter' in software keyboard). * - * Used to trigger the authentication check when the user presses 'enter' after writing the password, - * or to throw the server test when the only field on screen is the URL input field. + * Used to trigger the authentication check when the user presses 'enter' after writing the + * password, or to throw the server test when the only field on screen is the URL input field. */ @Override public boolean onEditorAction(TextView inputField, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_DONE && inputField != null && inputField.equals(mPasswordInput)) { + if (actionId == EditorInfo.IME_ACTION_DONE && inputField != null && + inputField.equals(mPasswordInput)) { if (mOkButton.isEnabled()) { mOkButton.performClick(); } - } else if (actionId == EditorInfo.IME_ACTION_NEXT && inputField != null && inputField.equals(mHostUrlInput)) { - if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType()).equals(mAuthTokenType)) { + } else if (actionId == EditorInfo.IME_ACTION_NEXT && inputField != null && + inputField.equals(mHostUrlInput)) { + if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType()). + equals(mAuthTokenType)) { checkOcServer(); } } @@ -1626,8 +1680,10 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { final int x = (int) event.getX(); final int y = (int) event.getY(); final Rect bounds = rightDrawable.getBounds(); - if (x >= (view.getRight() - bounds.width() - fuzz) && x <= (view.getRight() - view.getPaddingRight() + fuzz) - && y >= (view.getPaddingTop() - fuzz) && y <= (view.getHeight() - view.getPaddingBottom()) + fuzz) { + if ( x >= (view.getRight() - bounds.width() - fuzz) && + x <= (view.getRight() - view.getPaddingRight() + fuzz) && + y >= (view.getPaddingTop() - fuzz) && + y <= (view.getHeight() - view.getPaddingBottom()) + fuzz) { return onDrawableTouch(event); } @@ -1676,7 +1732,8 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { @Override public boolean onTouchEvent(MotionEvent event) { - if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType()).equals(mAuthTokenType) && + if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType()). + equals(mAuthTokenType) && mHostUrlInput.hasFocus() && event.getAction() == MotionEvent.ACTION_DOWN) { checkOcServer(); } @@ -1687,13 +1744,16 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { /** * Show untrusted cert dialog */ - public void showUntrustedCertDialog(X509Certificate x509Certificate, SslError error, SslErrorHandler handler) { + public void showUntrustedCertDialog( + X509Certificate x509Certificate, SslError error, SslErrorHandler handler + ) { // Show a dialog with the certificate info SslUntrustedCertDialog dialog = null; if (x509Certificate == null) { dialog = SslUntrustedCertDialog.newInstanceForEmptySslError(error, handler); } else { - dialog = SslUntrustedCertDialog.newInstanceForFullSslError(x509Certificate, error, handler); + dialog = SslUntrustedCertDialog. + newInstanceForFullSslError(x509Certificate, error, handler); } FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); @@ -1707,7 +1767,8 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { */ private void showUntrustedCertDialog(RemoteOperationResult result) { // Show a dialog with the certificate info - SslUntrustedCertDialog dialog = SslUntrustedCertDialog.newInstanceForFullSslError((CertificateCombinedException)result.getException()); + SslUntrustedCertDialog dialog = SslUntrustedCertDialog. + newInstanceForFullSslError((CertificateCombinedException)result.getException()); FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); ft.addToBackStack(null); @@ -1721,7 +1782,8 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { public void onSavedCertificate() { Fragment fd = getSupportFragmentManager().findFragmentByTag(SAML_DIALOG_TAG); if (fd == null) { - // if SAML dialog is not shown, the SslDialog was shown due to an SSL error in the server check + // if SAML dialog is not shown, + // the SslDialog was shown due to an SSL error in the server check checkOcServer(); } } @@ -1771,7 +1833,9 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { @Override public void onServiceConnected(ComponentName component, IBinder service) { - if (component.equals(new ComponentName(AuthenticatorActivity.this, OperationsService.class))) { + if (component.equals( + new ComponentName(AuthenticatorActivity.this, OperationsService.class) + )) { //Log_OC.wtf(TAG, "Operations service connected"); mOperationsServiceBinder = (OperationsServiceBinder) service; @@ -1785,7 +1849,9 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { @Override public void onServiceDisconnected(ComponentName component) { - if (component.equals(new ComponentName(AuthenticatorActivity.this, OperationsService.class))) { + if (component.equals( + new ComponentName(AuthenticatorActivity.this, OperationsService.class) + )) { Log_OC.e(TAG, "Operations service crashed"); mOperationsServiceBinder = null; } @@ -1801,7 +1867,8 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { public void createAuthenticationDialog(WebView webView, HttpAuthHandler handler) { // Show a dialog with the certificate info - CredentialsDialogFragment dialog = CredentialsDialogFragment.newInstanceForCredentials(webView, handler); + CredentialsDialogFragment dialog = + CredentialsDialogFragment.newInstanceForCredentials(webView, handler); FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); ft.addToBackStack(null); @@ -1809,7 +1876,11 @@ SsoWebViewClientListener, OnSslUntrustedCertListener { dialog.show(ft, CREDENTIALS_DIALOG_TAG); if (!mIsFirstAuthAttempt) { - Toast.makeText(getApplicationContext(), getText(R.string.saml_authentication_wrong_pass), Toast.LENGTH_LONG).show(); + Toast.makeText( + getApplicationContext(), + getText(R.string.saml_authentication_wrong_pass), + Toast.LENGTH_LONG + ).show(); } else { mIsFirstAuthAttempt = false; } diff --git a/src/com/owncloud/android/datamodel/FileDataStorageManager.java b/src/com/owncloud/android/datamodel/FileDataStorageManager.java index 795004a166..5b1bef1b5d 100644 --- a/src/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/src/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -151,7 +151,7 @@ public class FileDataStorageManager { public Vector getFolderImages(OCFile folder) { Vector ret = new Vector(); if (folder != null) { - // TODO better implementation, filtering in the access to database (if possible) instead of here + // TODO better implementation, filtering in the access to database instead of here Vector tmp = getFolderContent(folder); OCFile current = null; for (int i=0; i updatedFiles, Collection filesToRemove) { + public void saveFolder( + OCFile folder, Collection updatedFiles, Collection filesToRemove + ) { - Log_OC.d(TAG, "Saving folder " + folder.getRemotePath() + " with " + updatedFiles.size() + " children and " + filesToRemove.size() + " files to remove"); + Log_OC.d(TAG, "Saving folder " + folder.getRemotePath() + " with " + updatedFiles.size() + + " children and " + filesToRemove.size() + " files to remove"); - ArrayList operations = new ArrayList(updatedFiles.size()); + ArrayList operations = + new ArrayList(updatedFiles.size()); // prepare operations to insert or update files to save in the given folder for (OCFile file : updatedFiles) { ContentValues cv = new ContentValues(); cv.put(ProviderTableMeta.FILE_MODIFIED, file.getModificationTimestamp()); - cv.put(ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, file.getModificationTimestampAtLastSyncForData()); + cv.put( + ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, + file.getModificationTimestampAtLastSyncForData() + ); cv.put(ProviderTableMeta.FILE_CREATION, file.getCreationTimestamp()); cv.put(ProviderTableMeta.FILE_CONTENT_LENGTH, file.getFileLength()); cv.put(ProviderTableMeta.FILE_CONTENT_TYPE, file.getMimetype()); @@ -302,29 +312,40 @@ public class FileDataStorageManager { } else { // adding a new file - operations.add(ContentProviderOperation.newInsert(ProviderTableMeta.CONTENT_URI).withValues(cv).build()); + operations.add(ContentProviderOperation.newInsert(ProviderTableMeta.CONTENT_URI). + withValues(cv).build()); } } // prepare operations to remove files in the given folder - String where = ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?" + " AND " + ProviderTableMeta.FILE_PATH + "=?"; + String where = ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?" + " AND " + + ProviderTableMeta.FILE_PATH + "=?"; String [] whereArgs = null; for (OCFile file : filesToRemove) { if (file.getParentId() == folder.getFileId()) { whereArgs = new String[]{mAccount.name, file.getRemotePath()}; //Uri.withAppendedPath(ProviderTableMeta.CONTENT_URI_FILE, "" + file.getFileId()); if (file.isFolder()) { - operations.add(ContentProviderOperation - .newDelete(ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, file.getFileId())).withSelection(where, whereArgs) - .build()); - // TODO remove local folder + operations.add(ContentProviderOperation.newDelete( + ContentUris.withAppendedId( + ProviderTableMeta.CONTENT_URI_DIR, file.getFileId() + ) + ).withSelection(where, whereArgs).build()); + + File localFolder = + new File(FileStorageUtils.getDefaultSavePathFor(mAccount.name, file)); + if (localFolder.exists()) { + removeLocalFolder(localFolder); + } } else { - operations.add(ContentProviderOperation - .newDelete(ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, file.getFileId())).withSelection(where, whereArgs) - .build()); + operations.add(ContentProviderOperation.newDelete( + ContentUris.withAppendedId( + ProviderTableMeta.CONTENT_URI_FILE, file.getFileId() + ) + ).withSelection(where, whereArgs).build()); + if (file.isDown()) { new File(file.getStoragePath()).delete(); - // TODO move the deletion of local contents after success of deletions } } } @@ -333,9 +354,12 @@ public class FileDataStorageManager { // update metadata of folder ContentValues cv = new ContentValues(); cv.put(ProviderTableMeta.FILE_MODIFIED, folder.getModificationTimestamp()); - cv.put(ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, folder.getModificationTimestampAtLastSyncForData()); + cv.put( + ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, + folder.getModificationTimestampAtLastSyncForData() + ); cv.put(ProviderTableMeta.FILE_CREATION, folder.getCreationTimestamp()); - cv.put(ProviderTableMeta.FILE_CONTENT_LENGTH, 0); // FileContentProvider calculates the right size + cv.put(ProviderTableMeta.FILE_CONTENT_LENGTH, 0); cv.put(ProviderTableMeta.FILE_CONTENT_TYPE, folder.getMimetype()); cv.put(ProviderTableMeta.FILE_NAME, folder.getFileName()); cv.put(ProviderTableMeta.FILE_PARENT, folder.getParentId()); @@ -409,18 +433,21 @@ public class FileDataStorageManager { // Log_OC.d(TAG, "Updating size of " + id); // if (getContentResolver() != null) { // getContentResolver().update(ProviderTableMeta.CONTENT_URI_DIR, -// new ContentValues(), // won't be used, but cannot be null; crashes in KLP +// new ContentValues(), + // won't be used, but cannot be null; crashes in KLP // ProviderTableMeta._ID + "=?", // new String[] { String.valueOf(id) }); // } else { // try { // getContentProviderClient().update(ProviderTableMeta.CONTENT_URI_DIR, -// new ContentValues(), // won't be used, but cannot be null; crashes in KLP +// new ContentValues(), + // won't be used, but cannot be null; crashes in KLP // ProviderTableMeta._ID + "=?", // new String[] { String.valueOf(id) }); // // } catch (RemoteException e) { -// Log_OC.e(TAG, "Exception in update of folder size through compatibility patch " + e.getMessage()); +// Log_OC.e( +// TAG, "Exception in update of folder size through compatibility patch " + e.getMessage()); // } // } // } else { @@ -437,9 +464,12 @@ public class FileDataStorageManager { } else { if (removeDBData) { - //Uri file_uri = Uri.withAppendedPath(ProviderTableMeta.CONTENT_URI_FILE, ""+file.getFileId()); - Uri file_uri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, file.getFileId()); - String where = ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?" + " AND " + ProviderTableMeta.FILE_PATH + "=?"; + Uri file_uri = ContentUris.withAppendedId( + ProviderTableMeta.CONTENT_URI_FILE, + file.getFileId() + ); + String where = ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?" + " AND " + + ProviderTableMeta.FILE_PATH + "=?"; String [] whereArgs = new String[]{mAccount.name, file.getRemotePath()}; int deleted = 0; if (getContentProviderClient() != null) { @@ -481,8 +511,10 @@ public class FileDataStorageManager { } private boolean removeFolderInDb(OCFile folder) { - Uri folder_uri = Uri.withAppendedPath(ProviderTableMeta.CONTENT_URI_DIR, ""+ folder.getFileId()); // URI for recursive deletion - String where = ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?" + " AND " + ProviderTableMeta.FILE_PATH + "=?"; + Uri folder_uri = Uri.withAppendedPath(ProviderTableMeta.CONTENT_URI_DIR, "" + + folder.getFileId()); // URI for recursive deletion + String where = ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?" + " AND " + + ProviderTableMeta.FILE_PATH + "=?"; String [] whereArgs = new String[]{mAccount.name, folder.getRemotePath()}; int deleted = 0; if (getContentProviderClient() != null) { @@ -552,43 +584,67 @@ public class FileDataStorageManager { public void moveFolder(OCFile folder, String newPath) { // TODO check newPath - if (folder != null && folder.isFolder() && folder.fileExists() && !OCFile.ROOT_PATH.equals(folder.getFileName())) { + if ( folder != null && folder.isFolder() && + folder.fileExists() && !OCFile.ROOT_PATH.equals(folder.getFileName()) + ) { /// 1. get all the descendants of 'dir' in a single QUERY (including 'dir') Cursor c = null; if (getContentProviderClient() != null) { try { - c = getContentProviderClient().query(ProviderTableMeta.CONTENT_URI, - null, - ProviderTableMeta.FILE_ACCOUNT_OWNER + "=? AND " + ProviderTableMeta.FILE_PATH + " LIKE ? ", - new String[] { mAccount.name, folder.getRemotePath() + "%" }, ProviderTableMeta.FILE_PATH + " ASC "); + c = getContentProviderClient().query ( + ProviderTableMeta.CONTENT_URI, + null, + ProviderTableMeta.FILE_ACCOUNT_OWNER + "=? AND " + + ProviderTableMeta.FILE_PATH + " LIKE ? ", + new String[] { mAccount.name, folder.getRemotePath() + "%" }, + ProviderTableMeta.FILE_PATH + " ASC " + ); } catch (RemoteException e) { Log_OC.e(TAG, e.getMessage()); } } else { - c = getContentResolver().query(ProviderTableMeta.CONTENT_URI, - null, - ProviderTableMeta.FILE_ACCOUNT_OWNER + "=? AND " + ProviderTableMeta.FILE_PATH + " LIKE ? ", - new String[] { mAccount.name, folder.getRemotePath() + "%" }, ProviderTableMeta.FILE_PATH + " ASC "); + c = getContentResolver().query ( + ProviderTableMeta.CONTENT_URI, + null, + ProviderTableMeta.FILE_ACCOUNT_OWNER + "=? AND " + + ProviderTableMeta.FILE_PATH + " LIKE ? ", + new String[] { mAccount.name, folder.getRemotePath() + "%" }, + ProviderTableMeta.FILE_PATH + " ASC " + ); } /// 2. prepare a batch of update operations to change all the descendants - ArrayList operations = new ArrayList(c.getCount()); + ArrayList operations = + new ArrayList(c.getCount()); int lengthOfOldPath = folder.getRemotePath().length(); String defaultSavePath = FileStorageUtils.getSavePath(mAccount.name); int lengthOfOldStoragePath = defaultSavePath.length() + lengthOfOldPath; if (c.moveToFirst()) { do { - ContentValues cv = new ContentValues(); // don't take the constructor out of the loop and clear the object + ContentValues cv = new ContentValues(); // keep the constructor in the loop OCFile child = createFileInstance(c); - cv.put(ProviderTableMeta.FILE_PATH, newPath + child.getRemotePath().substring(lengthOfOldPath)); - if (child.getStoragePath() != null && child.getStoragePath().startsWith(defaultSavePath)) { - cv.put(ProviderTableMeta.FILE_STORAGE_PATH, defaultSavePath + newPath + child.getStoragePath().substring(lengthOfOldStoragePath)); + cv.put( + ProviderTableMeta.FILE_PATH, + newPath + child.getRemotePath().substring(lengthOfOldPath) + ); + if ( child.getStoragePath() != null && + child.getStoragePath().startsWith(defaultSavePath) ) { + cv.put( + ProviderTableMeta.FILE_STORAGE_PATH, + defaultSavePath + newPath + + child.getStoragePath().substring(lengthOfOldStoragePath) + ); } - operations.add(ContentProviderOperation.newUpdate(ProviderTableMeta.CONTENT_URI). + operations.add( + ContentProviderOperation. + newUpdate(ProviderTableMeta.CONTENT_URI). withValues(cv). - withSelection( ProviderTableMeta._ID + "=?", - new String[] { String.valueOf(child.getFileId()) }) - .build()); + withSelection( + ProviderTableMeta._ID + "=?", + new String[] { String.valueOf(child.getFileId()) } + ). + build() + ); } while (c.moveToNext()); } c.close(); @@ -603,10 +659,12 @@ public class FileDataStorageManager { } } catch (OperationApplicationException e) { - Log_OC.e(TAG, "Fail to update descendants of " + folder.getFileId() + " in database", e); + Log_OC.e(TAG, "Fail to update descendants of " + + folder.getFileId() + " in database", e); } catch (RemoteException e) { - Log_OC.e(TAG, "Fail to update desendants of " + folder.getFileId() + " in database", e); + Log_OC.e(TAG, "Fail to update desendants of " + + folder.getFileId() + " in database", e); } } @@ -851,7 +909,9 @@ public class FileDataStorageManager { file.setStoragePath(c.getString(c .getColumnIndex(ProviderTableMeta.FILE_STORAGE_PATH))); if (file.getStoragePath() == null) { - // try to find existing file and bind it with current account; - with the current update of SynchronizeFolderOperation, this won't be necessary anymore after a full synchronization of the account + // try to find existing file and bind it with current account; + // with the current update of SynchronizeFolderOperation, this won't be + // necessary anymore after a full synchronization of the account File f = new File(FileStorageUtils.getDefaultSavePathFor(mAccount.name, file)); if (f.exists()) { file.setStoragePath(f.getAbsolutePath()); @@ -930,13 +990,16 @@ public class FileDataStorageManager { cv.put(ProviderTableMeta.OCSHARES_SHARED_DATE, share.getSharedDate()); cv.put(ProviderTableMeta.OCSHARES_EXPIRATION_DATE, share.getExpirationDate()); cv.put(ProviderTableMeta.OCSHARES_TOKEN, share.getToken()); - cv.put(ProviderTableMeta.OCSHARES_SHARE_WITH_DISPLAY_NAME, share.getSharedWithDisplayName()); + cv.put( + ProviderTableMeta.OCSHARES_SHARE_WITH_DISPLAY_NAME, + share.getSharedWithDisplayName() + ); cv.put(ProviderTableMeta.OCSHARES_IS_DIRECTORY, share.isFolder() ? 1 : 0); cv.put(ProviderTableMeta.OCSHARES_USER_ID, share.getUserId()); cv.put(ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED, share.getIdRemoteShared()); cv.put(ProviderTableMeta.OCSHARES_ACCOUNT_OWNER, mAccount.name); - if (shareExists(share.getIdRemoteShared())) { // for renamed files; no more delete and create + if (shareExists(share.getIdRemoteShared())) { // for renamed files overriden = true; if (getContentResolver() != null) { @@ -1038,7 +1101,9 @@ public class FileDataStorageManager { share.setIsFolder(c.getInt( c.getColumnIndex(ProviderTableMeta.OCSHARES_IS_DIRECTORY)) == 1 ? true : false); share.setUserId(c.getLong(c.getColumnIndex(ProviderTableMeta.OCSHARES_USER_ID))); - share.setIdRemoteShared(c.getLong(c.getColumnIndex(ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED))); + share.setIdRemoteShared( + c.getLong(c.getColumnIndex(ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED)) + ); } return share; @@ -1090,7 +1155,9 @@ public class FileDataStorageManager { } else { try { - getContentProviderClient().update(ProviderTableMeta.CONTENT_URI, cv, where, whereArgs); + getContentProviderClient().update( + ProviderTableMeta.CONTENT_URI, cv, where, whereArgs + ); } catch (RemoteException e) { Log_OC.e(TAG, "Exception in cleanSharedFiles" + e.getMessage()); @@ -1102,7 +1169,8 @@ public class FileDataStorageManager { ContentValues cv = new ContentValues(); cv.put(ProviderTableMeta.FILE_SHARE_BY_LINK, false); cv.put(ProviderTableMeta.FILE_PUBLIC_LINK, ""); - String where = ProviderTableMeta.FILE_ACCOUNT_OWNER + "=? AND " + ProviderTableMeta.FILE_PARENT + "=?"; + String where = ProviderTableMeta.FILE_ACCOUNT_OWNER + "=? AND " + + ProviderTableMeta.FILE_PARENT + "=?"; String [] whereArgs = new String[] { mAccount.name , String.valueOf(folder.getFileId()) }; if (getContentResolver() != null) { @@ -1110,7 +1178,9 @@ public class FileDataStorageManager { } else { try { - getContentProviderClient().update(ProviderTableMeta.CONTENT_URI, cv, where, whereArgs); + getContentProviderClient().update( + ProviderTableMeta.CONTENT_URI, cv, where, whereArgs + ); } catch (RemoteException e) { Log_OC.e(TAG, "Exception in cleanSharedFilesInFolder " + e.getMessage()); @@ -1127,7 +1197,9 @@ public class FileDataStorageManager { } else { try { - getContentProviderClient().delete(ProviderTableMeta.CONTENT_URI_SHARE, where, whereArgs); + getContentProviderClient().delete( + ProviderTableMeta.CONTENT_URI_SHARE, where, whereArgs + ); } catch (RemoteException e) { Log_OC.e(TAG, "Exception in cleanShares" + e.getMessage()); @@ -1138,7 +1210,8 @@ public class FileDataStorageManager { public void saveShares(Collection shares) { cleanShares(); if (shares != null) { - ArrayList operations = new ArrayList(shares.size()); + ArrayList operations = + new ArrayList(shares.size()); // prepare operations to insert or update files to save in the given folder for (OCShare share : shares) { @@ -1152,7 +1225,10 @@ public class FileDataStorageManager { cv.put(ProviderTableMeta.OCSHARES_SHARED_DATE, share.getSharedDate()); cv.put(ProviderTableMeta.OCSHARES_EXPIRATION_DATE, share.getExpirationDate()); cv.put(ProviderTableMeta.OCSHARES_TOKEN, share.getToken()); - cv.put(ProviderTableMeta.OCSHARES_SHARE_WITH_DISPLAY_NAME, share.getSharedWithDisplayName()); + cv.put( + ProviderTableMeta.OCSHARES_SHARE_WITH_DISPLAY_NAME, + share.getSharedWithDisplayName() + ); cv.put(ProviderTableMeta.OCSHARES_IS_DIRECTORY, share.isFolder() ? 1 : 0); cv.put(ProviderTableMeta.OCSHARES_USER_ID, share.getUserId()); cv.put(ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED, share.getIdRemoteShared()); @@ -1160,15 +1236,23 @@ public class FileDataStorageManager { if (shareExists(share.getIdRemoteShared())) { // updating an existing file - operations.add(ContentProviderOperation.newUpdate(ProviderTableMeta.CONTENT_URI_SHARE). + operations.add( + ContentProviderOperation.newUpdate(ProviderTableMeta.CONTENT_URI_SHARE). withValues(cv). - withSelection( ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED + "=?", - new String[] { String.valueOf(share.getIdRemoteShared()) }) - .build()); + withSelection( + ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED + "=?", + new String[] { String.valueOf(share.getIdRemoteShared()) } + ). + build() + ); } else { // adding a new file - operations.add(ContentProviderOperation.newInsert(ProviderTableMeta.CONTENT_URI_SHARE).withValues(cv).build()); + operations.add( + ContentProviderOperation.newInsert(ProviderTableMeta.CONTENT_URI_SHARE). + withValues(cv). + build() + ); } } @@ -1176,10 +1260,13 @@ public class FileDataStorageManager { if (operations.size() > 0) { @SuppressWarnings("unused") ContentProviderResult[] results = null; - Log_OC.d(TAG, "Sending " + operations.size() + " operations to FileContentProvider"); + Log_OC.d(TAG, "Sending " + operations.size() + + " operations to FileContentProvider"); try { if (getContentResolver() != null) { - results = getContentResolver().applyBatch(MainApp.getAuthority(), operations); + results = getContentResolver().applyBatch( + MainApp.getAuthority(), operations + ); } else { results = getContentProviderClient().applyBatch(operations); @@ -1200,13 +1287,17 @@ public class FileDataStorageManager { cleanSharedFiles(); if (sharedFiles != null) { - ArrayList operations = new ArrayList(sharedFiles.size()); + ArrayList operations = + new ArrayList(sharedFiles.size()); // prepare operations to insert or update files to save in the given folder for (OCFile file : sharedFiles) { ContentValues cv = new ContentValues(); cv.put(ProviderTableMeta.FILE_MODIFIED, file.getModificationTimestamp()); - cv.put(ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, file.getModificationTimestampAtLastSyncForData()); + cv.put( + ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, + file.getModificationTimestampAtLastSyncForData() + ); cv.put(ProviderTableMeta.FILE_CREATION, file.getCreationTimestamp()); cv.put(ProviderTableMeta.FILE_CONTENT_LENGTH, file.getFileLength()); cv.put(ProviderTableMeta.FILE_CONTENT_TYPE, file.getMimetype()); @@ -1218,27 +1309,40 @@ public class FileDataStorageManager { } cv.put(ProviderTableMeta.FILE_ACCOUNT_OWNER, mAccount.name); cv.put(ProviderTableMeta.FILE_LAST_SYNC_DATE, file.getLastSyncDateForProperties()); - cv.put(ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA, file.getLastSyncDateForData()); + cv.put( + ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA, + file.getLastSyncDateForData() + ); cv.put(ProviderTableMeta.FILE_KEEP_IN_SYNC, file.keepInSync() ? 1 : 0); cv.put(ProviderTableMeta.FILE_ETAG, file.getEtag()); cv.put(ProviderTableMeta.FILE_SHARE_BY_LINK, file.isShareByLink() ? 1 : 0); cv.put(ProviderTableMeta.FILE_PUBLIC_LINK, file.getPublicLink()); cv.put(ProviderTableMeta.FILE_PERMISSIONS, file.getPermissions()); cv.put(ProviderTableMeta.FILE_REMOTE_ID, file.getRemoteId()); - cv.put(ProviderTableMeta.FILE_UPDATE_THUMBNAIL, file.needsUpdateThumbnail() ? 1 : 0); + cv.put( + ProviderTableMeta.FILE_UPDATE_THUMBNAIL, + file.needsUpdateThumbnail() ? 1 : 0 + ); boolean existsByPath = fileExists(file.getRemotePath()); if (existsByPath || fileExists(file.getFileId())) { // updating an existing file - operations.add(ContentProviderOperation.newUpdate(ProviderTableMeta.CONTENT_URI). + operations.add( + ContentProviderOperation.newUpdate(ProviderTableMeta.CONTENT_URI). withValues(cv). - withSelection( ProviderTableMeta._ID + "=?", - new String[] { String.valueOf(file.getFileId()) }) - .build()); + withSelection( + ProviderTableMeta._ID + "=?", + new String[] { String.valueOf(file.getFileId()) } + ).build() + ); } else { // adding a new file - operations.add(ContentProviderOperation.newInsert(ProviderTableMeta.CONTENT_URI).withValues(cv).build()); + operations.add( + ContentProviderOperation.newInsert(ProviderTableMeta.CONTENT_URI). + withValues(cv). + build() + ); } } @@ -1246,10 +1350,13 @@ public class FileDataStorageManager { if (operations.size() > 0) { @SuppressWarnings("unused") ContentProviderResult[] results = null; - Log_OC.d(TAG, "Sending " + operations.size() + " operations to FileContentProvider"); + Log_OC.d(TAG, "Sending " + operations.size() + + " operations to FileContentProvider"); try { if (getContentResolver() != null) { - results = getContentResolver().applyBatch(MainApp.getAuthority(), operations); + results = getContentResolver().applyBatch( + MainApp.getAuthority(), operations + ); } else { results = getContentProviderClient().applyBatch(operations); @@ -1268,7 +1375,8 @@ public class FileDataStorageManager { public void removeShare(OCShare share){ Uri share_uri = ProviderTableMeta.CONTENT_URI_SHARE; - String where = ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + "=?" + " AND " + ProviderTableMeta.FILE_PATH + "=?"; + String where = ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + "=?" + " AND " + + ProviderTableMeta.FILE_PATH + "=?"; String [] whereArgs = new String[]{mAccount.name, share.getPath()}; if (getContentProviderClient() != null) { try { @@ -1325,7 +1433,10 @@ public class FileDataStorageManager { cv.put(ProviderTableMeta.OCSHARES_SHARED_DATE, share.getSharedDate()); cv.put(ProviderTableMeta.OCSHARES_EXPIRATION_DATE, share.getExpirationDate()); cv.put(ProviderTableMeta.OCSHARES_TOKEN, share.getToken()); - cv.put(ProviderTableMeta.OCSHARES_SHARE_WITH_DISPLAY_NAME, share.getSharedWithDisplayName()); + cv.put( + ProviderTableMeta.OCSHARES_SHARE_WITH_DISPLAY_NAME, + share.getSharedWithDisplayName() + ); cv.put(ProviderTableMeta.OCSHARES_IS_DIRECTORY, share.isFolder() ? 1 : 0); cv.put(ProviderTableMeta.OCSHARES_USER_ID, share.getUserId()); cv.put(ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED, share.getIdRemoteShared()); @@ -1334,7 +1445,8 @@ public class FileDataStorageManager { /* if (shareExists(share.getIdRemoteShared())) { // updating an existing share resource - operations.add(ContentProviderOperation.newUpdate(ProviderTableMeta.CONTENT_URI_SHARE). + operations.add( + ContentProviderOperation.newUpdate(ProviderTableMeta.CONTENT_URI_SHARE). withValues(cv). withSelection( ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED + "=?", new String[] { String.valueOf(share.getIdRemoteShared()) }) @@ -1343,7 +1455,11 @@ public class FileDataStorageManager { } else { */ // adding a new share resource - operations.add(ContentProviderOperation.newInsert(ProviderTableMeta.CONTENT_URI_SHARE).withValues(cv).build()); + operations.add( + ContentProviderOperation.newInsert(ProviderTableMeta.CONTENT_URI_SHARE). + withValues(cv). + build() + ); //} } } @@ -1372,18 +1488,23 @@ public class FileDataStorageManager { } - private ArrayList prepareRemoveSharesInFolder(OCFile folder, ArrayList preparedOperations) { + private ArrayList prepareRemoveSharesInFolder( + OCFile folder, ArrayList preparedOperations + ) { if (folder != null) { - String where = ProviderTableMeta.OCSHARES_PATH + "=?" + " AND " + ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + "=?"; + String where = ProviderTableMeta.OCSHARES_PATH + "=?" + " AND " + + ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + "=?"; String [] whereArgs = new String[]{ "", mAccount.name }; Vector files = getFolderContent(folder); for (OCFile file : files) { whereArgs[0] = file.getRemotePath(); - preparedOperations.add(ContentProviderOperation.newDelete(ProviderTableMeta.CONTENT_URI_SHARE) - .withSelection(where, whereArgs) - .build()); + preparedOperations.add( + ContentProviderOperation.newDelete(ProviderTableMeta.CONTENT_URI_SHARE). + withSelection(where, whereArgs). + build() + ); } } return preparedOperations; diff --git a/src/com/owncloud/android/datamodel/ThumbnailsCacheManager.java b/src/com/owncloud/android/datamodel/ThumbnailsCacheManager.java new file mode 100644 index 0000000000..e75404ef3b --- /dev/null +++ b/src/com/owncloud/android/datamodel/ThumbnailsCacheManager.java @@ -0,0 +1,265 @@ +/* ownCloud Android client application + * Copyright (C) 2012-2014 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.datamodel; + +import java.io.File; +import java.lang.ref.WeakReference; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.media.ThumbnailUtils; +import android.os.AsyncTask; +import android.util.TypedValue; +import android.widget.ImageView; + +import com.owncloud.android.MainApp; +import com.owncloud.android.lib.common.utils.Log_OC; +import com.owncloud.android.ui.adapter.DiskLruImageCache; +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 + */ +public class ThumbnailsCacheManager { + + private static final String TAG = ThumbnailsCacheManager.class.getSimpleName(); + + private static final String CACHE_FOLDER = "thumbnailCache"; + + private static final Object mThumbnailsDiskCacheLock = new Object(); + private static DiskLruImageCache mThumbnailCache = null; + private static boolean mThumbnailCacheStarting = true; + + private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB + private static final CompressFormat mCompressFormat = CompressFormat.JPEG; + private static final int mCompressQuality = 70; + + public static Bitmap mDefaultImg = + BitmapFactory.decodeResource( + MainApp.getAppContext().getResources(), + DisplayUtils.getResourceId("image/png", "default.png") + ); + + + public static class InitDiskCacheTask extends AsyncTask { + @Override + protected Void doInBackground(File... params) { + synchronized (mThumbnailsDiskCacheLock) { + mThumbnailCacheStarting = true; + if (mThumbnailCache == null) { + try { + // Check if media is mounted or storage is built-in, if so, + // try and use external cache dir; otherwise use internal cache dir + final String cachePath = + MainApp.getAppContext().getExternalCacheDir().getPath() + + File.separator + CACHE_FOLDER; + Log_OC.d(TAG, "create dir: " + cachePath); + final File diskCacheDir = new File(cachePath); + mThumbnailCache = new DiskLruImageCache( + diskCacheDir, + DISK_CACHE_SIZE, + mCompressFormat, + mCompressQuality + ); + } catch (Exception e) { + Log_OC.d(TAG, "Thumbnail cache could not be opened ", e); + mThumbnailCache = null; + } + } + mThumbnailCacheStarting = false; // Finished initialization + mThumbnailsDiskCacheLock.notifyAll(); // Wake any waiting threads + } + return null; + } + } + + + public static void addBitmapToCache(String key, Bitmap bitmap) { + synchronized (mThumbnailsDiskCacheLock) { + if (mThumbnailCache != null) { + mThumbnailCache.put(key, bitmap); + } + } + } + + + public static Bitmap getBitmapFromDiskCache(String key) { + synchronized (mThumbnailsDiskCacheLock) { + // Wait while disk cache is started from background thread + while (mThumbnailCacheStarting) { + try { + mThumbnailsDiskCacheLock.wait(); + } catch (InterruptedException e) {} + } + if (mThumbnailCache != null) { + return (Bitmap) mThumbnailCache.getBitmap(key); + } + } + 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 { + private final WeakReference mImageViewReference; + private OCFile mFile; + private FileDataStorageManager mStorageManager; + + public ThumbnailGenerationTask(ImageView imageView, FileDataStorageManager storageManager) { + // 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"); + mStorageManager = storageManager; + } + + // Decode image in background. + @Override + protected Bitmap doInBackground(OCFile... params) { + Bitmap thumbnail = null; + + try { + 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(TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 150, r.getDisplayMetrics() + )); + + if (mFile.isDown()){ + Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile( + mFile.getStoragePath(), px, px); + + if (bitmap != null) { + thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px); + + // Add thumbnail to cache + addBitmapToCache(imageKey, thumbnail); + + mFile.setNeedsUpdateThumbnail(false); + mStorageManager.saveFile(mFile); + } + + } + } + + } 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; + } + + if (mImageViewReference != null && bitmap != null) { + final ImageView imageView = mImageViewReference.get(); + final ThumbnailGenerationTask bitmapWorkerTask = + getBitmapWorkerTask(imageView); + if (this == bitmapWorkerTask && imageView != null) { + if (imageView.getTag().equals(mFile.getFileId())) { + imageView.setImageBitmap(bitmap); + } + } + } + } + } + + + 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); + } + + 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/files/services/FileDownloader.java b/src/com/owncloud/android/files/services/FileDownloader.java index cb0f12ceb6..fdc35f8d9c 100644 --- a/src/com/owncloud/android/files/services/FileDownloader.java +++ b/src/com/owncloud/android/files/services/FileDownloader.java @@ -398,6 +398,7 @@ public class FileDownloader extends Service implements OnDatatransferProgressLis file.setMimetype(mCurrentDownload.getMimeType()); file.setStoragePath(mCurrentDownload.getSavePath()); file.setFileLength((new File(mCurrentDownload.getSavePath()).length())); + file.setRemoteId(mCurrentDownload.getFile().getRemoteId()); mStorageManager.saveFile(file); } diff --git a/src/com/owncloud/android/files/services/FileUploader.java b/src/com/owncloud/android/files/services/FileUploader.java index 5b7185ac8f..ad2a2cbe44 100644 --- a/src/com/owncloud/android/files/services/FileUploader.java +++ b/src/com/owncloud/android/files/services/FileUploader.java @@ -630,7 +630,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe // coincidence; nothing else is needed, the storagePath is right // in the instance returned by mCurrentUpload.getFile() } - + file.setNeedsUpdateThumbnail(true); mStorageManager.saveFile(file); } @@ -641,6 +641,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe file.setModificationTimestamp(remoteFile.getModifiedTimestamp()); file.setModificationTimestampAtLastSyncForData(remoteFile.getModifiedTimestamp()); // file.setEtag(remoteFile.getEtag()); // TODO Etag, where available + file.setRemoteId(remoteFile.getRemoteId()); } private OCFile obtainNewOCFileToUpload(String remotePath, String localPath, String mimeType, diff --git a/src/com/owncloud/android/operations/CreateFolderOperation.java b/src/com/owncloud/android/operations/CreateFolderOperation.java index b0e7ed9de4..4df8b3df1f 100644 --- a/src/com/owncloud/android/operations/CreateFolderOperation.java +++ b/src/com/owncloud/android/operations/CreateFolderOperation.java @@ -84,21 +84,36 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper } } - /** * Save new directory in local database */ public void saveFolderInDB() { - OCFile newDir = new OCFile(mRemotePath); - newDir.setMimetype("DIR"); - long parentId = getStorageManager().getFileByPath(FileStorageUtils.getParentPath(mRemotePath)).getFileId(); - newDir.setParentId(parentId); - newDir.setModificationTimestamp(System.currentTimeMillis()); - getStorageManager().saveFile(newDir); + if (mCreateFullPath && getStorageManager(). + getFileByPath(FileStorageUtils.getParentPath(mRemotePath)) == null){// When parent + // of remote path + // is not created + String[] subFolders = mRemotePath.split("/"); + String composedRemotePath = "/"; - Log_OC.d(TAG, "Create directory " + mRemotePath + " in Database"); + // For each antecesor folders create them recursively + for (int i=0; i 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 the folder should be refreshed also */ + /** 'True' means that Share resources bound to the files into should be refreshed also */ private boolean mIsShareSupported; - /** 'True' means that the remote folder changed from last synchronization and should be fetched */ + /** 'True' means that the remote folder changed and should be fetched */ private boolean mRemoteFolderChanged; /** 'True' means that Etag will be ignored */ @@ -118,9 +123,12 @@ public class SynchronizeFolderOperation extends RemoteOperation { * * @param remoteFolderPath Remote folder to synchronize. * @param currentSyncTime Time stamp for the synchronization process in progress. - * @param localFolderId Identifier in the local database of the folder to synchronize. - * @param updateFolderProperties 'True' means that the properties of the folder should be updated also, not just its content. - * @param syncFullAccount 'True' means that this operation is part of a full account synchronization. + * @param localFolderId Identifier in the local database of the folder + * to synchronize. + * @param updateFolderProperties 'True' means that the properties of the folder should + * be updated also, not just its content. + * @param syncFullAccount 'True' means that this operation is part of a full account + * synchronization. * @param dataStorageManager Interface with the local database. * @param account ownCloud account where the folder is located. * @param context Application context. @@ -159,7 +167,8 @@ public class SynchronizeFolderOperation extends RemoteOperation { } /** - * Returns the list of files and folders contained in the synchronized folder, if called after synchronization is complete. + * 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. */ @@ -194,7 +203,9 @@ public class SynchronizeFolderOperation extends RemoteOperation { } if (!mSyncFullAccount) { - sendLocalBroadcast(EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result); + sendLocalBroadcast( + EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result + ); } if (result.isSuccess() && mIsShareSupported && !mSyncFullAccount) { @@ -202,7 +213,9 @@ public class SynchronizeFolderOperation extends RemoteOperation { } if (!mSyncFullAccount) { - sendLocalBroadcast(EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result); + sendLocalBroadcast( + EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result + ); } return result; @@ -235,12 +248,14 @@ public class SynchronizeFolderOperation extends RemoteOperation { if (!mIgnoreETag) { // check if remote and local folder are different - mRemoteFolderChanged = !(remoteFolder.getEtag().equalsIgnoreCase(mLocalFolder.getEtag())); + mRemoteFolderChanged = + !(remoteFolder.getEtag().equalsIgnoreCase(mLocalFolder.getEtag())); } result = new RemoteOperationResult(ResultCode.OK); - Log_OC.i(TAG, "Checked " + mAccount.name + remotePath + " : " + (mRemoteFolderChanged ? "changed" : "not changed")); + Log_OC.i(TAG, "Checked " + mAccount.name + remotePath + " : " + + (mRemoteFolderChanged ? "changed" : "not changed")); } else { // check failed @@ -248,9 +263,11 @@ public class SynchronizeFolderOperation extends RemoteOperation { removeLocalFolder(); } if (result.isException()) { - Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + result.getLogMessage(), result.getException()); + Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + + result.getLogMessage(), result.getException()); } else { - Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + result.getLogMessage()); + Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + + result.getLogMessage()); } } @@ -267,7 +284,8 @@ public class SynchronizeFolderOperation extends RemoteOperation { if (result.isSuccess()) { synchronizeData(result.getData(), client); if (mConflictsFound > 0 || mFailsInFavouritesFound > 0) { - result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); // should be different result, but will do the job + 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) @@ -281,7 +299,13 @@ public class SynchronizeFolderOperation extends RemoteOperation { private void removeLocalFolder() { if (mStorageManager.fileExists(mLocalFolder.getFileId())) { String currentSavePath = FileStorageUtils.getSavePath(mAccount.name); - mStorageManager.removeFolder(mLocalFolder, true, (mLocalFolder.isDown() && mLocalFolder.getStoragePath().startsWith(currentSavePath))); + mStorageManager.removeFolder( + mLocalFolder, + true, + ( mLocalFolder.isDown() && + mLocalFolder.getStoragePath().startsWith(currentSavePath) + ) + ); } } @@ -296,7 +320,7 @@ public class SynchronizeFolderOperation extends RemoteOperation { * * @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. + * @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 @@ -307,7 +331,8 @@ public class SynchronizeFolderOperation extends RemoteOperation { remoteFolder.setParentId(mLocalFolder.getParentId()); remoteFolder.setFileId(mLocalFolder.getFileId()); - Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath() + " changed - starting update of local data "); + 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(); @@ -327,30 +352,38 @@ public class SynchronizeFolderOperation extends RemoteOperation { remoteFile.setParentId(mLocalFolder.getFileId()); /// retrieve local data for the read file - //localFile = mStorageManager.getFileByPath(remoteFile.getRemotePath()); + // localFile = mStorageManager.getFileByPath(remoteFile.getRemotePath()); localFile = localFilesMap.remove(remoteFile.getRemotePath()); - /// add to the remoteFile (the new one) data about LOCAL STATE (not existing in the server side) + /// add to the remoteFile (the new one) data about LOCAL STATE (not existing in server) remoteFile.setLastSyncDateForProperties(mCurrentSyncTime); if (localFile != null) { // some properties of local state are kept unmodified remoteFile.setFileId(localFile.getFileId()); remoteFile.setKeepInSync(localFile.keepInSync()); remoteFile.setLastSyncDateForData(localFile.getLastSyncDateForData()); - remoteFile.setModificationTimestampAtLastSyncForData(localFile.getModificationTimestampAtLastSyncForData()); + remoteFile.setModificationTimestampAtLastSyncForData( + localFile.getModificationTimestampAtLastSyncForData() + ); remoteFile.setStoragePath(localFile.getStoragePath()); - remoteFile.setEtag(localFile.getEtag()); // eTag will not be updated unless contents are synchronized (Synchronize[File|Folder]Operation with remoteFile as parameter) + // eTag will not be updated unless contents are synchronized + // (Synchronize[File|Folder]Operation with remoteFile as parameter) + remoteFile.setEtag(localFile.getEtag()); if (remoteFile.isFolder()) { - remoteFile.setFileLength(localFile.getFileLength()); // TODO move operations about size of folders to FileContentProvider + remoteFile.setFileLength(localFile.getFileLength()); + // TODO move operations about size of folders to FileContentProvider } remoteFile.setPublicLink(localFile.getPublicLink()); remoteFile.setShareByLink(localFile.isShareByLink()); } else { - remoteFile.setEtag(""); // remote eTag will not be updated unless contents are synchronized (Synchronize[File|Folder]Operation with remoteFile as parameter) + // remote eTag will not be updated unless contents are synchronized + // (Synchronize[File|Folder]Operation with remoteFile as parameter) + remoteFile.setEtag(""); } /// check and fix, if needed, local storage path - checkAndFixForeignStoragePath(remoteFile); // fixing old policy - now local files must be copied into the ownCloud local folder + checkAndFixForeignStoragePath(remoteFile); // policy - local files are COPIED + // into the ownCloud local folder; searchForLocalFileInDefaultPath(remoteFile); // legacy /// prepare content synchronization for kept-in-sync files @@ -368,7 +401,7 @@ public class SynchronizeFolderOperation extends RemoteOperation { updatedFiles.add(remoteFile); } - // save updated contents in local database; all at once, trying to get a best performance in database update (not a big deal, indeed) + // save updated contents in local database mStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values()); // request for the synchronization of file contents AFTER saving current remote properties @@ -378,27 +411,32 @@ public class SynchronizeFolderOperation extends RemoteOperation { } /** - * 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. + * Performs a list of synchronization operations, determining if a download or upload is needed + * or if exists conflict due to changes both in local and remote contents of the each file. * - * If download or upload is needed, request the operation to the corresponding service and goes on. + * 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 + ) { RemoteOperationResult contentsResult = null; for (SynchronizeFileOperation op: filesToSyncContents) { - contentsResult = op.execute(mStorageManager, mContext); // returns without waiting for upload or download finishes + 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()); + Log_OC.e(TAG, "Error while synchronizing favourites : " + + contentsResult.getLogMessage(), contentsResult.getException()); } else { - Log_OC.e(TAG, "Error while synchronizing favourites : " + contentsResult.getLogMessage()); + Log_OC.e(TAG, "Error while synchronizing favourites : " + + contentsResult.getLogMessage()); } } } // won't let these fails break the synchronization process @@ -430,11 +468,11 @@ public class SynchronizeFolderOperation extends RemoteOperation { /** - * 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. + * 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} + * 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. */ @@ -456,7 +494,9 @@ public class SynchronizeFolderOperation extends RemoteOperation { File expectedParent = expectedFile.getParentFile(); expectedParent.mkdirs(); if (!expectedParent.isDirectory()) { - throw new IOException("Unexpected error: parent directory could not be created"); + throw new IOException( + "Unexpected error: parent directory could not be created" + ); } expectedFile.createNewFile(); if (!expectedFile.isFile()) { @@ -480,12 +520,14 @@ public class SynchronizeFolderOperation extends RemoteOperation { try { if (in != null) in.close(); } catch (Exception e) { - Log_OC.d(TAG, "Weird exception while closing input stream for " + storagePath + " (ignoring)", 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); + Log_OC.d(TAG, "Weird exception while closing output stream for " + + expectedPath + " (ignoring)", e); } } } @@ -497,7 +539,8 @@ public class SynchronizeFolderOperation extends RemoteOperation { RemoteOperationResult result = null; // remote request - GetRemoteSharesForFileOperation operation = new GetRemoteSharesForFileOperation(mLocalFolder.getRemotePath(), false, true); + GetRemoteSharesForFileOperation operation = + new GetRemoteSharesForFileOperation(mLocalFolder.getRemotePath(), false, true); result = operation.execute(client); if (result.isSuccess()) { @@ -532,13 +575,17 @@ public class SynchronizeFolderOperation extends RemoteOperation { /** - * Sends a message to any application component interested in the progress of the synchronization. + * 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 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) { + 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); diff --git a/src/com/owncloud/android/providers/FileContentProvider.java b/src/com/owncloud/android/providers/FileContentProvider.java index 9792f3e93a..a94454ceed 100644 --- a/src/com/owncloud/android/providers/FileContentProvider.java +++ b/src/com/owncloud/android/providers/FileContentProvider.java @@ -22,13 +22,12 @@ import java.util.ArrayList; import java.util.HashMap; import com.owncloud.android.R; +import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.db.ProviderMeta; import com.owncloud.android.db.ProviderMeta.ProviderTableMeta; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.shares.ShareType; - - import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; @@ -165,13 +164,14 @@ public class FileContentProvider extends ContentProvider { int count = 0; switch (mUriMatcher.match(uri)) { case SINGLE_FILE: - /*Cursor c = query(db, uri, null, where, whereArgs, null); - String remotePath = "(unexisting)"; + Cursor c = query(db, uri, null, where, whereArgs, null); + String remoteId = ""; if (c != null && c.moveToFirst()) { - remotePath = c.getString(c.getColumnIndex(ProviderTableMeta.FILE_PATH)); + remoteId = c.getString(c.getColumnIndex(ProviderTableMeta.FILE_REMOTE_ID)); + //ThumbnailsCacheManager.removeFileFromCache(remoteId); } - Log_OC.d(TAG, "Removing FILE " + remotePath); - */ + Log_OC.d(TAG, "Removing FILE " + remoteId); + count = db.delete(ProviderTableMeta.FILE_TABLE_NAME, ProviderTableMeta._ID + "=" @@ -197,16 +197,28 @@ public class FileContentProvider extends ContentProvider { Cursor children = query(uri, null, null, null, null); if (children != null && children.moveToFirst()) { long childId; - boolean isDir; + boolean isDir; //String remotePath; while (!children.isAfterLast()) { childId = children.getLong(children.getColumnIndex(ProviderTableMeta._ID)); - isDir = "DIR".equals(children.getString(children.getColumnIndex(ProviderTableMeta.FILE_CONTENT_TYPE))); + isDir = "DIR".equals(children.getString( + children.getColumnIndex(ProviderTableMeta.FILE_CONTENT_TYPE) + )); //remotePath = children.getString(children.getColumnIndex(ProviderTableMeta.FILE_PATH)); if (isDir) { - count += delete(db, ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, childId), null, null); + count += delete( + db, + ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, childId), + null, + null + ); } else { - count += delete(db, ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, childId), null, null); + count += delete( + db, + ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, childId), + null, + null + ); } children.moveToNext(); } @@ -240,7 +252,6 @@ public class FileContentProvider extends ContentProvider { } return count; } - @Override public String getType(Uri uri) { @@ -257,7 +268,6 @@ public class FileContentProvider extends ContentProvider { @Override public Uri insert(Uri uri, ContentValues values) { - //Log_OC.d(TAG, "Inserting " + values.getAsString(ProviderTableMeta.FILE_PATH) + " at provider " + this); Uri newUri = null; SQLiteDatabase db = mDbHelper.getWritableDatabase(); db.beginTransaction(); @@ -277,23 +287,31 @@ public class FileContentProvider extends ContentProvider { case SINGLE_FILE: String remotePath = values.getAsString(ProviderTableMeta.FILE_PATH); String accountName = values.getAsString(ProviderTableMeta.FILE_ACCOUNT_OWNER); - String[] projection = new String[] {ProviderTableMeta._ID, ProviderTableMeta.FILE_PATH, ProviderTableMeta.FILE_ACCOUNT_OWNER }; - String where = ProviderTableMeta.FILE_PATH + "=? AND " + ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?"; + String[] projection = new String[] { + ProviderTableMeta._ID, ProviderTableMeta.FILE_PATH, + ProviderTableMeta.FILE_ACCOUNT_OWNER + }; + String where = ProviderTableMeta.FILE_PATH + "=? AND " + + ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?"; String[] whereArgs = new String[] {remotePath, accountName}; Cursor doubleCheck = query(db, uri, projection, where, whereArgs, null); - if (doubleCheck == null || !doubleCheck.moveToFirst()) { // ugly patch; serious refactorization is needed to reduce work in FileDataStorageManager and bring it to FileContentProvider + // ugly patch; serious refactorization is needed to reduce work in + // FileDataStorageManager and bring it to FileContentProvider + if (doubleCheck == null || !doubleCheck.moveToFirst()) { long rowId = db.insert(ProviderTableMeta.FILE_TABLE_NAME, null, values); if (rowId > 0) { - Uri insertedFileUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, rowId); - //Log_OC.d(TAG, "Inserted " + values.getAsString(ProviderTableMeta.FILE_PATH) + " at provider " + this); + Uri insertedFileUri = + ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, rowId); return insertedFileUri; } else { - //Log_OC.d(TAG, "Error while inserting " + values.getAsString(ProviderTableMeta.FILE_PATH) + " at provider " + this); throw new SQLException("ERROR " + uri); } } else { // file is already inserted; race condition, let's avoid a duplicated entry - Uri insertedFileUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, doubleCheck.getLong(doubleCheck.getColumnIndex(ProviderTableMeta._ID))); + Uri insertedFileUri = ContentUris.withAppendedId( + ProviderTableMeta.CONTENT_URI_FILE, + doubleCheck.getLong(doubleCheck.getColumnIndex(ProviderTableMeta._ID)) + ); doubleCheck.close(); return insertedFileUri; @@ -302,22 +320,35 @@ public class FileContentProvider extends ContentProvider { case SHARES: String path = values.getAsString(ProviderTableMeta.OCSHARES_PATH); String accountNameShare= values.getAsString(ProviderTableMeta.OCSHARES_ACCOUNT_OWNER); - String[] projectionShare = new String[] {ProviderTableMeta._ID, ProviderTableMeta.OCSHARES_PATH, ProviderTableMeta.OCSHARES_ACCOUNT_OWNER }; - String whereShare = ProviderTableMeta.OCSHARES_PATH + "=? AND " + ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + "=?"; + String[] projectionShare = new String[] { + ProviderTableMeta._ID, ProviderTableMeta.OCSHARES_PATH, + ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + }; + String whereShare = ProviderTableMeta.OCSHARES_PATH + "=? AND " + + ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + "=?"; String[] whereArgsShare = new String[] {path, accountNameShare}; Uri insertedShareUri = null; - Cursor doubleCheckShare = query(db, uri, projectionShare, whereShare, whereArgsShare, null); - if (doubleCheckShare == null || !doubleCheckShare.moveToFirst()) { // ugly patch; serious refactorization is needed to reduce work in FileDataStorageManager and bring it to FileContentProvider + Cursor doubleCheckShare = + query(db, uri, projectionShare, whereShare, whereArgsShare, null); + // ugly patch; serious refactorization is needed to reduce work in + // FileDataStorageManager and bring it to FileContentProvider + if (doubleCheckShare == null || !doubleCheckShare.moveToFirst()) { long rowId = db.insert(ProviderTableMeta.OCSHARES_TABLE_NAME, null, values); if (rowId >0) { - insertedShareUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_SHARE, rowId); + insertedShareUri = + ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_SHARE, rowId); } else { throw new SQLException("ERROR " + uri); } } else { // file is already inserted; race condition, let's avoid a duplicated entry - insertedShareUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_SHARE, doubleCheckShare.getLong(doubleCheckShare.getColumnIndex(ProviderTableMeta._ID))); + insertedShareUri = ContentUris.withAppendedId( + ProviderTableMeta.CONTENT_URI_SHARE, + doubleCheckShare.getLong( + doubleCheckShare.getColumnIndex(ProviderTableMeta._ID) + ) + ); doubleCheckShare.close(); } updateFilesTableAccordingToShareInsertion(db, uri, values); @@ -330,11 +361,17 @@ public class FileContentProvider extends ContentProvider { } - private void updateFilesTableAccordingToShareInsertion(SQLiteDatabase db, Uri uri, ContentValues shareValues) { + private void updateFilesTableAccordingToShareInsertion( + SQLiteDatabase db, Uri uri, ContentValues shareValues + ) { ContentValues fileValues = new ContentValues(); - fileValues.put(ProviderTableMeta.FILE_SHARE_BY_LINK, - ShareType.PUBLIC_LINK.getValue() == shareValues.getAsInteger(ProviderTableMeta.OCSHARES_SHARE_TYPE)? 1 : 0); - String whereShare = ProviderTableMeta.FILE_PATH + "=? AND " + ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?"; + fileValues.put( + ProviderTableMeta.FILE_SHARE_BY_LINK, + ShareType.PUBLIC_LINK.getValue() == + shareValues.getAsInteger(ProviderTableMeta.OCSHARES_SHARE_TYPE)? 1 : 0 + ); + String whereShare = ProviderTableMeta.FILE_PATH + "=? AND " + + ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?"; String[] whereArgsShare = new String[] { shareValues.getAsString(ProviderTableMeta.OCSHARES_PATH), shareValues.getAsString(ProviderTableMeta.OCSHARES_ACCOUNT_OWNER) @@ -362,7 +399,14 @@ public class FileContentProvider extends ContentProvider { @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + public Cursor query( + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder + ) { + Cursor result = null; SQLiteDatabase db = mDbHelper.getReadableDatabase(); db.beginTransaction(); @@ -375,7 +419,15 @@ public class FileContentProvider extends ContentProvider { return result; } - private Cursor query(SQLiteDatabase db, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + private Cursor query( + SQLiteDatabase db, + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder + ) { + SQLiteQueryBuilder sqlQuery = new SQLiteQueryBuilder(); sqlQuery.setTables(ProviderTableMeta.FILE_TABLE_NAME); @@ -429,7 +481,6 @@ public class FileContentProvider extends ContentProvider { @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - //Log_OC.d(TAG, "Updating " + values.getAsString(ProviderTableMeta.FILE_PATH) + " at provider " + this); int count = 0; SQLiteDatabase db = mDbHelper.getWritableDatabase(); db.beginTransaction(); @@ -445,14 +496,24 @@ public class FileContentProvider extends ContentProvider { - private int update(SQLiteDatabase db, Uri uri, ContentValues values, String selection, String[] selectionArgs) { + private int update( + SQLiteDatabase db, + Uri uri, + ContentValues values, + String selection, + String[] selectionArgs + ) { switch (mUriMatcher.match(uri)) { case DIRECTORY: return 0; //updateFolderSize(db, selectionArgs[0]); case SHARES: - return db.update(ProviderTableMeta.OCSHARES_TABLE_NAME, values, selection, selectionArgs); + return db.update( + ProviderTableMeta.OCSHARES_TABLE_NAME, values, selection, selectionArgs + ); default: - return db.update(ProviderTableMeta.FILE_TABLE_NAME, values, selection, selectionArgs); + return db.update( + ProviderTableMeta.FILE_TABLE_NAME, values, selection, selectionArgs + ); } } @@ -510,8 +571,10 @@ public class FileContentProvider extends ContentProvider { */ @Override - public ContentProviderResult[] applyBatch (ArrayList operations) throws OperationApplicationException { - Log_OC.d("FileContentProvider", "applying batch in provider " + this + " (temporary: " + isTemporary() + ")" ); + public ContentProviderResult[] applyBatch (ArrayList operations) + throws OperationApplicationException { + Log_OC.d("FileContentProvider", "applying batch in provider " + this + + " (temporary: " + isTemporary() + ")" ); ContentProviderResult[] results = new ContentProviderResult[operations.size()]; int i=0; @@ -600,12 +663,13 @@ public class FileContentProvider extends ContentProvider { db.beginTransaction(); try { db.execSQL("ALTER TABLE " + ProviderTableMeta.FILE_TABLE_NAME + - " ADD COLUMN " + ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA + " INTEGER " + - " DEFAULT 0"); + " ADD COLUMN " + ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA + + " INTEGER " + " DEFAULT 0"); // assume there are not local changes pending to upload db.execSQL("UPDATE " + ProviderTableMeta.FILE_TABLE_NAME + - " SET " + ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA + " = " + System.currentTimeMillis() + + " SET " + ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA + " = " + + System.currentTimeMillis() + " WHERE " + ProviderTableMeta.FILE_STORAGE_PATH + " IS NOT NULL"); upgraded = true; @@ -619,11 +683,12 @@ public class FileContentProvider extends ContentProvider { db.beginTransaction(); try { db .execSQL("ALTER TABLE " + ProviderTableMeta.FILE_TABLE_NAME + - " ADD COLUMN " + ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA + " INTEGER " + - " DEFAULT 0"); + " ADD COLUMN " + ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA + + " INTEGER " + " DEFAULT 0"); db.execSQL("UPDATE " + ProviderTableMeta.FILE_TABLE_NAME + - " SET " + ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA + " = " + ProviderTableMeta.FILE_MODIFIED + + " SET " + ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA + " = " + + ProviderTableMeta.FILE_MODIFIED + " WHERE " + ProviderTableMeta.FILE_STORAGE_PATH + " IS NOT NULL"); upgraded = true; @@ -633,7 +698,8 @@ public class FileContentProvider extends ContentProvider { } } if (!upgraded) - Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + ", newVersion == " + newVersion); + Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + + ", newVersion == " + newVersion); if (oldVersion < 5 && newVersion >= 5) { Log_OC.i("SQL", "Entering in the #4 ADD in onUpgrade"); @@ -650,7 +716,8 @@ public class FileContentProvider extends ContentProvider { } } if (!upgraded) - Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + ", newVersion == " + newVersion); + Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + + ", newVersion == " + newVersion); if (oldVersion < 6 && newVersion >= 6) { Log_OC.i("SQL", "Entering in the #5 ADD in onUpgrade"); @@ -689,7 +756,8 @@ public class FileContentProvider extends ContentProvider { } } if (!upgraded) - Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + ", newVersion == " + newVersion); + Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + + ", newVersion == " + newVersion); if (oldVersion < 7 && newVersion >= 7) { Log_OC.i("SQL", "Entering in the #7 ADD in onUpgrade"); @@ -710,7 +778,8 @@ public class FileContentProvider extends ContentProvider { } } if (!upgraded) - Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + ", newVersion == " + newVersion); + Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + + ", newVersion == " + newVersion); if (oldVersion < 8 && newVersion >= 8) { Log_OC.i("SQL", "Entering in the #8 ADD in onUpgrade"); @@ -727,7 +796,8 @@ public class FileContentProvider extends ContentProvider { } } if (!upgraded) - Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + ", newVersion == " + newVersion); + Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + + ", newVersion == " + newVersion); } } diff --git a/src/com/owncloud/android/ui/activity/MoveActivity.java b/src/com/owncloud/android/ui/activity/MoveActivity.java index 2d6824c892..8a254705b9 100644 --- a/src/com/owncloud/android/ui/activity/MoveActivity.java +++ b/src/com/owncloud/android/ui/activity/MoveActivity.java @@ -31,7 +31,6 @@ import android.content.res.Resources.NotFoundException; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; -import android.support.v4.widget.SwipeRefreshLayout; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; @@ -64,7 +63,7 @@ import com.owncloud.android.utils.ErrorMessageAdapter; import com.owncloud.android.lib.common.utils.Log_OC; public class MoveActivity extends HookActivity implements FileFragment.ContainerActivity, - OnClickListener, SwipeRefreshLayout.OnRefreshListener { + OnClickListener, OnEnforceableRefreshListener { public static final String EXTRA_CURRENT_FOLDER = UploadFilesActivity.class.getCanonicalName() + ".EXTRA_CURRENT_FOLDER"; public static final String EXTRA_TARGET_FILE = UploadFilesActivity.class.getCanonicalName() + "EXTRA_TARGET_FILE"; @@ -554,16 +553,23 @@ public class MoveActivity extends HookActivity implements FileFragment.Container } - @Override public void onRefresh() { + refreshList(true); + } + + @Override + public void onRefresh(boolean enforced) { + refreshList(enforced); + } + + private void refreshList(boolean ignoreETag) { OCFileListFragment listOfFiles = getListOfFilesFragment(); if (listOfFiles != null) { OCFile folder = listOfFiles.getCurrentFile(); if (folder != null) { - startSyncFolderOperation(folder, true); + startSyncFolderOperation(folder, ignoreETag); } } } - } diff --git a/src/com/owncloud/android/ui/activity/Preferences.java b/src/com/owncloud/android/ui/activity/Preferences.java index 47ef32cc88..20330931b4 100644 --- a/src/com/owncloud/android/ui/activity/Preferences.java +++ b/src/com/owncloud/android/ui/activity/Preferences.java @@ -74,6 +74,7 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa private final Handler mHandler = new Handler(); private String mAccountName; private boolean mShowContextMenu = false; + private String mUploadPath; @SuppressWarnings("deprecation") @@ -87,7 +88,9 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa actionBar.setIcon(DisplayUtils.getSeasonalIconId()); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.actionbar_settings); - + + loadInstantUploadPath(); + // Load the accounts category for adding the list of accounts mAccountsPrefCategory = (PreferenceCategory) findPreference("accounts_category"); @@ -239,6 +242,16 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa preferenceCategory.removePreference(pImprint); } } + + Preference pInstantUploadPathApp = (Preference) findPreference("instant_upload_path"); + + pInstantUploadPathApp.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + mUploadPath = updateInstantUploadPath(newValue.toString()); + return true; + } + }); /* About App */ pAboutApp = (Preference) findPreference("about_app"); @@ -254,6 +267,12 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa } } + @Override + protected void onPause() { + saveInstantUploadPathOnPreferences(); + super.onPause(); + } + @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { @@ -463,4 +482,47 @@ public class Preferences extends SherlockPreferenceActivity implements AccountMa } + /** + * Update the upload path checking that it is a correct path + * @param uploadPath: path write by user + * @return String: uploadPath + */ + private String updateInstantUploadPath(String uploadPath) { + String slashString = "/"; + + // If slashes are duplicated, replace them for only one slash + uploadPath = uploadPath.replaceAll("/+", slashString); + + // Remove last slash from path + if (uploadPath.length() > 0 && uploadPath.charAt(uploadPath.length()-1) == slashString.charAt(0)) { + uploadPath = uploadPath.substring(0, uploadPath.length()-1); + } + + if (uploadPath.isEmpty()) { // Set default instant upload path + uploadPath = getString(R.string.instant_upload_path); + }else { + if (!uploadPath.startsWith(slashString)) { // Add initial slash on path if necessary + uploadPath = slashString.concat(uploadPath); + } + } + return uploadPath; + } + + /** + * Load upload path set on preferences + */ + private void loadInstantUploadPath() { + SharedPreferences appPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + mUploadPath = appPrefs.getString("instant_upload_path", getString(R.string.instant_upload_path)); + } + + /** + * Save the "Instant Upload Path" on preferences + */ + private void saveInstantUploadPathOnPreferences() { + SharedPreferences appPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + SharedPreferences.Editor editor = appPrefs.edit(); + editor.putString("instant_upload_path", mUploadPath); + editor.commit(); + } } diff --git a/src/com/owncloud/android/ui/adapter/DiskLruImageCache.java b/src/com/owncloud/android/ui/adapter/DiskLruImageCache.java index d22dc971c6..93efdf1cd3 100644 --- a/src/com/owncloud/android/ui/adapter/DiskLruImageCache.java +++ b/src/com/owncloud/android/ui/adapter/DiskLruImageCache.java @@ -1,3 +1,20 @@ +/* ownCloud Android client application + * Copyright (C) 2012-2014 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + package com.owncloud.android.ui.adapter; import java.io.BufferedInputStream; @@ -7,14 +24,10 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; -import android.util.Log; import com.jakewharton.disklrucache.DiskLruCache; import com.owncloud.android.BuildConfig; @@ -28,16 +41,14 @@ public class DiskLruImageCache { private static final int CACHE_VERSION = 1; private static final int VALUE_COUNT = 1; private static final int IO_BUFFER_SIZE = 8 * 1024; - private static final Pattern CAPITAL_LETTERS = Pattern.compile("[A-Z]"); - - private StringBuffer mValidKeyBuffer = new StringBuffer(64); - private StringBuffer mConversionBuffer = new StringBuffer(2).append('_'); - private static final String TAG = "DiskLruImageCache"; + private static final String TAG = DiskLruImageCache.class.getSimpleName(); + + //public DiskLruImageCache( Context context,String uniqueName, int diskCacheSize, + public DiskLruImageCache( + File diskCacheDir, int diskCacheSize, CompressFormat compressFormat, int quality + ) throws IOException { - public DiskLruImageCache( Context context,String uniqueName, int diskCacheSize, - CompressFormat compressFormat, int quality ) throws IOException { - final File diskCacheDir = getDiskCacheDir(context, uniqueName ); mDiskCache = DiskLruCache.open( diskCacheDir, CACHE_VERSION, VALUE_COUNT, diskCacheSize ); @@ -58,17 +69,6 @@ public class DiskLruImageCache { } } - private File getDiskCacheDir(Context context, String uniqueName) { - - // Check if media is mounted or storage is built-in, if so, try and use external cache dir - // otherwise use internal cache dir - final String cachePath = context.getExternalCacheDir().getPath(); - - Log_OC.d(TAG, "create dir: " + cachePath + File.separator + uniqueName); - - return new File(cachePath + File.separator + uniqueName); - } - public void put( String key, Bitmap data ) { DiskLruCache.Editor editor = null; @@ -83,17 +83,17 @@ public class DiskLruImageCache { mDiskCache.flush(); editor.commit(); if ( BuildConfig.DEBUG ) { - Log.d( "cache_test_DISK_", "image put on disk cache " + validKey ); + Log_OC.d( "cache_test_DISK_", "image put on disk cache " + validKey ); } } else { editor.abort(); if ( BuildConfig.DEBUG ) { - Log.d( "cache_test_DISK_", "ERROR on: image put on disk cache " + validKey ); + Log_OC.d( "cache_test_DISK_", "ERROR on: image put on disk cache " + validKey ); } } } catch (IOException e) { if ( BuildConfig.DEBUG ) { - Log.d( "cache_test_DISK_", "ERROR on: image put on disk cache " + validKey ); + Log_OC.d( "cache_test_DISK_", "ERROR on: image put on disk cache " + validKey ); } try { if ( editor != null ) { @@ -131,7 +131,8 @@ public class DiskLruImageCache { } if ( BuildConfig.DEBUG ) { - Log.d("cache_test_DISK_", bitmap == null ? "not found" : "image read from disk " + validKey); + Log_OC.d("cache_test_DISK_", bitmap == null ? + "not found" : "image read from disk " + validKey); } return bitmap; @@ -160,7 +161,7 @@ public class DiskLruImageCache { public void clearCache() { if ( BuildConfig.DEBUG ) { - Log.d( "cache_test_DISK_", "disk cache CLEARED"); + Log_OC.d( "cache_test_DISK_", "disk cache CLEARED"); } try { mDiskCache.delete(); @@ -174,16 +175,20 @@ public class DiskLruImageCache { } private String convertToValidKey(String key) { - Matcher capitalLettersMatcher = CAPITAL_LETTERS.matcher(key); - mValidKeyBuffer.delete(0, mValidKeyBuffer.length()); - mConversionBuffer.delete(1, mConversionBuffer.length()); - - while (capitalLettersMatcher.find()) { - mConversionBuffer.replace(1, 2, capitalLettersMatcher.group(0).toLowerCase()); - capitalLettersMatcher.appendReplacement(mValidKeyBuffer, mConversionBuffer.toString()); - } - capitalLettersMatcher.appendTail(mValidKeyBuffer); - return mValidKeyBuffer.toString(); + return Integer.toString(key.hashCode()); } + /** + * Remove passed key from cache + * @param key + */ + public void removeKey( String key ) { + String validKey = convertToValidKey(key); + try { + mDiskCache.remove(validKey); + Log_OC.d(TAG, "removeKey from cache: " + validKey); + } catch (IOException e) { + e.printStackTrace(); + } + } } \ No newline at end of file diff --git a/src/com/owncloud/android/ui/adapter/FileListListAdapter.java b/src/com/owncloud/android/ui/adapter/FileListListAdapter.java index ffdad17573..0d10e7fa36 100644 --- a/src/com/owncloud/android/ui/adapter/FileListListAdapter.java +++ b/src/com/owncloud/android/ui/adapter/FileListListAdapter.java @@ -17,21 +17,11 @@ */ package com.owncloud.android.ui.adapter; -import java.io.File; -import java.lang.ref.WeakReference; import java.util.Vector; import android.accounts.Account; import android.content.Context; -import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Bitmap.CompressFormat; -import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.media.ThumbnailUtils; -import android.os.AsyncTask; -import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -45,11 +35,11 @@ import com.owncloud.android.R; 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.lib.common.utils.Log_OC; import com.owncloud.android.ui.activity.ComponentsGetter; -import com.owncloud.android.utils.BitmapUtils; import com.owncloud.android.utils.DisplayUtils; @@ -64,8 +54,6 @@ import com.owncloud.android.utils.DisplayUtils; public class FileListListAdapter extends BaseAdapter implements ListAdapter { private final static String PERMISSION_SHARED_WITH_ME = "S"; - private static final String TAG = FileListListAdapter.class.getSimpleName(); - private Context mContext; private OCFile mFile = null; private Vector mFiles = null; @@ -75,14 +63,6 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter { private Account mAccount; private ComponentsGetter mTransferServiceGetter; - private final Object thumbnailDiskCacheLock = new Object(); - private DiskLruImageCache mThumbnailCache; - private boolean mThumbnailCacheStarting = true; - private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB - private static final CompressFormat mCompressFormat = CompressFormat.JPEG; - private static final int mCompressQuality = 70; - private Bitmap defaultImg; - public FileListListAdapter( boolean justFolders, Context context, @@ -93,143 +73,11 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter { mContext = context; mAccount = AccountUtils.getCurrentOwnCloudAccount(mContext); mTransferServiceGetter = transferServiceGetter; - defaultImg = BitmapFactory.decodeResource(mContext.getResources(), - DisplayUtils.getResourceId("image/png", "default.png")); - // Initialise disk cache on background thread - new InitDiskCacheTask().execute(); + // initialise thumbnails cache on background thread + new ThumbnailsCacheManager.InitDiskCacheTask().execute(); } - class InitDiskCacheTask extends AsyncTask { - @Override - protected Void doInBackground(File... params) { - synchronized (thumbnailDiskCacheLock) { - try { - mThumbnailCache = new DiskLruImageCache(mContext, "thumbnailCache", - DISK_CACHE_SIZE, mCompressFormat, mCompressQuality); - } catch (Exception e) { - Log_OC.d(TAG, "Thumbnail cache could not be opened ", e); - mThumbnailCache = null; - } - mThumbnailCacheStarting = false; // Finished initialization - thumbnailDiskCacheLock.notifyAll(); // Wake any waiting threads - } - return null; - } - } - - static class AsyncDrawable extends BitmapDrawable { - private final WeakReference bitmapWorkerTaskReference; - - public AsyncDrawable(Resources res, Bitmap bitmap, - ThumbnailGenerationTask bitmapWorkerTask) { - super(res, bitmap); - bitmapWorkerTaskReference = - new WeakReference(bitmapWorkerTask); - } - - public ThumbnailGenerationTask getBitmapWorkerTask() { - return bitmapWorkerTaskReference.get(); - } - } - - class ThumbnailGenerationTask extends AsyncTask { - private final WeakReference imageViewReference; - private OCFile file; - - - public ThumbnailGenerationTask(ImageView imageView) { - // Use a WeakReference to ensure the ImageView can be garbage collected - imageViewReference = new WeakReference(imageView); - } - - // Decode image in background. - @Override - protected Bitmap doInBackground(OCFile... params) { - Bitmap thumbnail = null; - - try { - file = params[0]; - 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()) { - // Converts dp to pixel - Resources r = mContext.getResources(); - int px = (int) Math.round(TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 150, r.getDisplayMetrics() - )); - - if (file.isDown()){ - Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile( - file.getStoragePath(), px, px); - - if (bitmap != null) { - thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px); - - // Add thumbnail to cache - addBitmapToCache(imageKey, thumbnail); - - file.setNeedsUpdateThumbnail(false); - mStorageManager.saveFile(file); - } - - } - } - - } catch (Throwable t) { - // the app should never break due to a problem with thumbnails - Log_OC.e(TAG, "Generation of thumbnail for " + file + " failed", t); - if (t instanceof OutOfMemoryError) { - System.gc(); - } - } - - return thumbnail; - } - - protected void onPostExecute(Bitmap bitmap){ - if (isCancelled()) { - bitmap = null; - } - - if (imageViewReference != null && bitmap != null) { - final ImageView imageView = imageViewReference.get(); - final ThumbnailGenerationTask bitmapWorkerTask = - getBitmapWorkerTask(imageView); - if (this == bitmapWorkerTask && imageView != null) { - imageView.setImageBitmap(bitmap); - } - } - } - } - - public void addBitmapToCache(String key, Bitmap bitmap) { - synchronized (thumbnailDiskCacheLock) { - if (mThumbnailCache != null) { - mThumbnailCache.put(key, bitmap); - } - } - } - - public Bitmap getBitmapFromDiskCache(String key) { - synchronized (thumbnailDiskCacheLock) { - // Wait while disk cache is started from background thread - while (mThumbnailCacheStarting) { - try { - thumbnailDiskCacheLock.wait(); - } catch (InterruptedException e) {} - } - if (mThumbnailCache != null) { - return (Bitmap) mThumbnailCache.getBitmap(key); - } - } - return null; - } - @Override public boolean areAllItemsEnabled() { return true; @@ -280,6 +128,7 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter { 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); @@ -334,18 +183,28 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter { } // get Thumbnail if file is image - if (file.isImage()){ + if (file.isImage() && file.getRemoteId() != null){ // Thumbnail in Cache? - Bitmap thumbnail = getBitmapFromDiskCache(String.valueOf(file.getRemoteId())); + Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( + String.valueOf(file.getRemoteId()) + ); if (thumbnail != null && !file.needsUpdateThumbnail()){ fileIcon.setImageBitmap(thumbnail); } else { // generate new Thumbnail - if (cancelPotentialWork(file, fileIcon)) { - final ThumbnailGenerationTask task = - new ThumbnailGenerationTask(fileIcon); - final AsyncDrawable asyncDrawable = - new AsyncDrawable(mContext.getResources(), defaultImg, task); + if (ThumbnailsCacheManager.cancelPotentialWork(file, fileIcon)) { + final ThumbnailsCacheManager.ThumbnailGenerationTask task = + new ThumbnailsCacheManager.ThumbnailGenerationTask( + fileIcon, mStorageManager + ); + if (thumbnail == null) { + thumbnail = ThumbnailsCacheManager.mDefaultImg; + } + final AsyncDrawable asyncDrawable = new AsyncDrawable( + mContext.getResources(), + thumbnail, + task + ); fileIcon.setImageDrawable(asyncDrawable); task.execute(file); } @@ -396,35 +255,6 @@ public class FileListListAdapter extends BaseAdapter implements ListAdapter { return view; } - public static boolean cancelPotentialWork(OCFile file, ImageView imageView) { - final ThumbnailGenerationTask bitmapWorkerTask = getBitmapWorkerTask(imageView); - - if (bitmapWorkerTask != null) { - final OCFile bitmapData = bitmapWorkerTask.file; - // 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; - } - - private 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; - } - @Override public int getViewTypeCount() { return 1; diff --git a/src/com/owncloud/android/ui/preview/FileDownloadFragment.java b/src/com/owncloud/android/ui/preview/FileDownloadFragment.java index b8489b599e..98bbda38fd 100644 --- a/src/com/owncloud/android/ui/preview/FileDownloadFragment.java +++ b/src/com/owncloud/android/ui/preview/FileDownloadFragment.java @@ -32,6 +32,7 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageButton; +import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; @@ -126,6 +127,13 @@ public class FileDownloadFragment extends FileFragment implements OnClickListene ((ImageButton)mView.findViewById(R.id.cancelBtn)).setOnClickListener(this); + ((LinearLayout)mView.findViewById(R.id.fileDownloadLL)).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + ((PreviewImageActivity) getActivity()).toggleFullScreen(); + } + }); + if (mError) { setButtonsForRemote(); } else { diff --git a/src/com/owncloud/android/ui/preview/ImageViewCustom.java b/src/com/owncloud/android/ui/preview/ImageViewCustom.java new file mode 100644 index 0000000000..ad85140464 --- /dev/null +++ b/src/com/owncloud/android/ui/preview/ImageViewCustom.java @@ -0,0 +1,70 @@ +package com.owncloud.android.ui.preview; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.os.Build; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; + +import com.owncloud.android.lib.common.utils.Log_OC; + +public class ImageViewCustom extends ImageView { + + private static final boolean IS_ICS_OR_HIGHER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH; + + private Bitmap mBitmap; + + + public ImageViewCustom(Context context) { + super(context); + } + + public ImageViewCustom(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ImageViewCustom(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @SuppressLint("NewApi") + @Override + protected void onDraw(Canvas canvas) { + + if(IS_ICS_OR_HIGHER && checkIfMaximumBitmapExceed(canvas)) { + // Set layer type to software one for avoiding exceed + // and problems in visualization + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + + super.onDraw(canvas); + } + + /** + * Checks if current bitmaps exceed the maximum OpenGL texture size limit + * @param bitmap + * @return boolean + */ + @SuppressLint("NewApi") + private boolean checkIfMaximumBitmapExceed(Canvas canvas) { + Log_OC.d("OC", "Canvas maximum: " + canvas.getMaximumBitmapWidth() + " - " + canvas.getMaximumBitmapHeight()); + if (mBitmap!= null && (mBitmap.getWidth() > canvas.getMaximumBitmapWidth() + || mBitmap.getHeight() > canvas.getMaximumBitmapHeight())) { + return true; + } + + return false; + } + + /** + * Set current bitmap + * @param bitmap + */ + public void setBitmap (Bitmap bitmap) { + mBitmap = bitmap; + } + +} diff --git a/src/com/owncloud/android/ui/preview/PreviewImageFragment.java b/src/com/owncloud/android/ui/preview/PreviewImageFragment.java index 8e3afaf5a0..4dd5c43679 100644 --- a/src/com/owncloud/android/ui/preview/PreviewImageFragment.java +++ b/src/com/owncloud/android/ui/preview/PreviewImageFragment.java @@ -16,6 +16,12 @@ */ package com.owncloud.android.ui.preview; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; import java.lang.ref.WeakReference; import android.accounts.Account; @@ -40,7 +46,6 @@ import android.widget.TextView; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; -import com.ortiz.touch.TouchImageView; import com.owncloud.android.R; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.files.FileMenuFilter; @@ -48,6 +53,8 @@ import com.owncloud.android.lib.common.utils.Log_OC; 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.TouchImageViewCustom; + /** @@ -60,12 +67,13 @@ import com.owncloud.android.ui.fragment.FileFragment; * @author David A. Velasco */ public class PreviewImageFragment extends FileFragment { + public static final String EXTRA_FILE = "FILE"; public static final String EXTRA_ACCOUNT = "ACCOUNT"; private View mView; private Account mAccount; - private TouchImageView mImageView; + private TouchImageViewCustom mImageView; private TextView mMessageView; private ProgressBar mProgressWheel; @@ -124,7 +132,7 @@ public class PreviewImageFragment extends FileFragment { Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); mView = inflater.inflate(R.layout.preview_image_fragment, container, false); - mImageView = (TouchImageView) mView.findViewById(R.id.image); + mImageView = (TouchImageViewCustom) mView.findViewById(R.id.image); mImageView.setVisibility(View.GONE); mImageView.setOnClickListener(new OnClickListener() { @Override @@ -306,6 +314,7 @@ public class PreviewImageFragment extends FileFragment { public void onDestroy() { if (mBitmap != null) { mBitmap.recycle(); + System.gc(); } super.onDestroy(); } @@ -327,7 +336,7 @@ public class PreviewImageFragment extends FileFragment { * * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes. */ - private final WeakReference mImageViewRef; + private final WeakReference mImageViewRef; /** * Weak reference to the target {@link TextView} where error messages will be written. @@ -356,65 +365,27 @@ public class PreviewImageFragment extends FileFragment { * * @param imageView Target {@link ImageView} where the bitmap will be loaded into. */ - public BitmapLoader(ImageView imageView, TextView messageView, ProgressBar progressWheel) { - mImageViewRef = new WeakReference(imageView); + public BitmapLoader(ImageViewCustom imageView, TextView messageView, ProgressBar progressWheel) { + mImageViewRef = new WeakReference(imageView); mMessageViewRef = new WeakReference(messageView); mProgressWheelRef = new WeakReference(progressWheel); } - @SuppressWarnings("deprecation") - @SuppressLint({ "NewApi", "NewApi", "NewApi" }) // to avoid Lint errors since Android SDK r20 - @Override + @Override protected Bitmap doInBackground(String... params) { Bitmap result = null; if (params.length != 1) return result; String storagePath = params[0]; try { - // set desired options that will affect the size of the bitmap - BitmapFactory.Options options = new Options(); - options.inScaled = true; - options.inPurgeable = true; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - options.inPreferQualityOverSpeed = false; - } - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) { - options.inMutable = false; - } - // make a false load of the bitmap - just to be able to read outWidth, outHeight and outMimeType - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(storagePath, options); - - int width = options.outWidth; - int height = options.outHeight; - int scale = 1; - - Display display = getActivity().getWindowManager().getDefaultDisplay(); - Point size = new Point(); - int screenWidth; - int screenHeight; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR2) { - display.getSize(size); - screenWidth = size.x; - screenHeight = size.y; - } else { - screenWidth = display.getWidth(); - screenHeight = display.getHeight(); - } - if (width > screenWidth) { - // second try to scale down the image , this time depending upon the screen size - scale = (int) Math.floor((float)width / screenWidth); - } - if (height > screenHeight) { - scale = Math.max(scale, (int) Math.floor((float)height / screenHeight)); - } - options.inSampleSize = scale; + File picture = new File(storagePath); - // really load the bitmap - options.inJustDecodeBounds = false; // the next decodeFile call will be real - result = BitmapFactory.decodeFile(storagePath, options); - //Log_OC.d(TAG, "Image loaded - width: " + options.outWidth + ", loaded height: " + options.outHeight); + if (picture != null) { + //Decode file into a bitmap in real size for being able to make zoom on the image + result = BitmapFactory.decodeStream(new FlushedInputStream + (new BufferedInputStream(new FileInputStream(picture)))); + } if (result == null) { mErrorMessageId = R.string.preview_image_error_unknown_format; @@ -422,8 +393,15 @@ public class PreviewImageFragment extends FileFragment { } } catch (OutOfMemoryError e) { - mErrorMessageId = R.string.preview_image_error_unknown_format; Log_OC.e(TAG, "Out of memory occured for file " + storagePath, e); + + // If out of memory error when loading image, try to load it scaled + result = loadScaledImage(storagePath); + + if (result == null) { + mErrorMessageId = R.string.preview_image_error_unknown_format; + Log_OC.e(TAG, "File could not be loaded as a bitmap: " + storagePath); + } } catch (NoSuchFieldError e) { mErrorMessageId = R.string.common_error_unknown; @@ -446,11 +424,13 @@ public class PreviewImageFragment extends FileFragment { showErrorMessage(); } } - + + @SuppressLint("InlinedApi") private void showLoadedImage(Bitmap result) { if (mImageViewRef != null) { - final ImageView imageView = mImageViewRef.get(); + final ImageViewCustom imageView = mImageViewRef.get(); if (imageView != null) { + imageView.setBitmap(result); imageView.setImageBitmap(result); imageView.setVisibility(View.VISIBLE); mBitmap = result; @@ -511,8 +491,87 @@ public class PreviewImageFragment extends FileFragment { container.finish(); } - public TouchImageView getImageView() { + public TouchImageViewCustom getImageView() { return mImageView; } - + + static class FlushedInputStream extends FilterInputStream { + public FlushedInputStream(InputStream inputStream) { + super(inputStream); + } + + @Override + public long skip(long n) throws IOException { + long totalBytesSkipped = 0L; + while (totalBytesSkipped < n) { + long bytesSkipped = in.skip(n - totalBytesSkipped); + if (bytesSkipped == 0L) { + int byteValue = read(); + if (byteValue < 0) { + break; // we reached EOF + } else { + bytesSkipped = 1; // we read one byte + } + } + totalBytesSkipped += bytesSkipped; + } + return totalBytesSkipped; + } + } + + /** + * Load image scaled + * @param storagePath: path of the image + * @return Bitmap + */ + @SuppressWarnings("deprecation") + private Bitmap loadScaledImage(String storagePath) { + + Log_OC.d(TAG, "Loading image scaled"); + + // set desired options that will affect the size of the bitmap + BitmapFactory.Options options = new Options(); + options.inScaled = true; + options.inPurgeable = true; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + options.inPreferQualityOverSpeed = false; + } + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) { + options.inMutable = false; + } + // make a false load of the bitmap - just to be able to read outWidth, outHeight and outMimeType + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(storagePath, options); + + int width = options.outWidth; + int height = options.outHeight; + int scale = 1; + + Display display = getActivity().getWindowManager().getDefaultDisplay(); + Point size = new Point(); + int screenWidth; + int screenHeight; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR2) { + display.getSize(size); + screenWidth = size.x; + screenHeight = size.y; + } else { + screenWidth = display.getWidth(); + screenHeight = display.getHeight(); + } + + if (width > screenWidth) { + // second try to scale down the image , this time depending upon the screen size + scale = (int) Math.floor((float)width / screenWidth); + } + if (height > screenHeight) { + scale = Math.max(scale, (int) Math.floor((float)height / screenHeight)); + } + options.inSampleSize = scale; + + // really load the bitmap + options.inJustDecodeBounds = false; // the next decodeFile call will be real + return BitmapFactory.decodeFile(storagePath, options); + + } } diff --git a/src/com/owncloud/android/ui/preview/PreviewMediaFragment.java b/src/com/owncloud/android/ui/preview/PreviewMediaFragment.java index ea412ced9f..7d6489b281 100644 --- a/src/com/owncloud/android/ui/preview/PreviewMediaFragment.java +++ b/src/com/owncloud/android/ui/preview/PreviewMediaFragment.java @@ -29,6 +29,7 @@ import android.media.MediaPlayer; import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnErrorListener; import android.media.MediaPlayer.OnPreparedListener; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.IBinder; @@ -372,13 +373,15 @@ public class PreviewMediaFragment extends FileFragment implements mVideoPreview.setOnErrorListener(mVideoHelper); } + @SuppressWarnings("static-access") private void playVideo() { // create and prepare control panel for the user mMediaController.setMediaPlayer(mVideoPreview); // load the video file in the video player ; // when done, VideoHelper#onPrepared() will be called - mVideoPreview.setVideoPath(getFile().getStoragePath()); + Uri uri = Uri.parse(getFile().getStoragePath()); + mVideoPreview.setVideoPath(uri.encode(getFile().getStoragePath())); } diff --git a/src/com/owncloud/android/utils/FileStorageUtils.java b/src/com/owncloud/android/utils/FileStorageUtils.java index 58dda0daf2..3895821d6c 100644 --- a/src/com/owncloud/android/utils/FileStorageUtils.java +++ b/src/com/owncloud/android/utils/FileStorageUtils.java @@ -26,6 +26,8 @@ import com.owncloud.android.lib.resources.files.RemoteFile; import android.annotation.SuppressLint; import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; import android.net.Uri; import android.os.Environment; import android.os.StatFs; @@ -73,7 +75,9 @@ public class FileStorageUtils { } public static String getInstantUploadFilePath(Context context, String fileName) { - String uploadPath = context.getString(R.string.instant_upload_path); + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); + String uploadPathdef = context.getString(R.string.instant_upload_path); + String uploadPath = pref.getString("instant_upload_path", uploadPathdef); String value = uploadPath + OCFile.PATH_SEPARATOR + (fileName == null ? "" : fileName); return value; } @@ -120,4 +124,4 @@ public class FileStorageUtils { return file; } -} \ No newline at end of file +} diff --git a/src/com/owncloud/android/utils/TouchImageViewCustom.java b/src/com/owncloud/android/utils/TouchImageViewCustom.java new file mode 100644 index 0000000000..a0f7b79276 --- /dev/null +++ b/src/com/owncloud/android/utils/TouchImageViewCustom.java @@ -0,0 +1,1276 @@ +/* + * 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