From 10520fb1bdd728170938a39f7067850ad1114120 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 2 Apr 2019 11:57:12 +0200 Subject: [PATCH 01/12] Upgrade string from Riot --- .../src/main/res/values-ca/strings.xml | 4 +- .../src/main/res/values-de/strings.xml | 2 +- .../src/main/res/values-nl/strings.xml | 88 +- .../src/main/res/values-pl/strings.xml | 8 +- .../src/main/res/values-sk/strings.xml | 8 +- .../src/main/res/values-sq/strings.xml | 8 +- vector/src/main/res/values-bg/strings.xml | 28 +- vector/src/main/res/values-bn-rIN/strings.xml | 340 ++++- vector/src/main/res/values-ca/strings.xml | 23 +- vector/src/main/res/values-de/strings.xml | 45 +- vector/src/main/res/values-eo/strings.xml | 23 +- vector/src/main/res/values-es/strings.xml | 94 +- vector/src/main/res/values-eu/strings.xml | 104 +- vector/src/main/res/values-fi/strings.xml | 52 +- vector/src/main/res/values-fr/strings.xml | 22 +- vector/src/main/res/values-hu/strings.xml | 22 +- vector/src/main/res/values-it/strings.xml | 58 +- vector/src/main/res/values-nl/strings.xml | 1312 +++++++++++------ vector/src/main/res/values-pl/strings.xml | 44 +- vector/src/main/res/values-pt-rBR/strings.xml | 14 +- vector/src/main/res/values-ru/strings.xml | 61 +- vector/src/main/res/values-sk/strings.xml | 141 +- vector/src/main/res/values-sq/strings.xml | 250 ++-- vector/src/main/res/values-tr/strings.xml | 2 +- vector/src/main/res/values-zh-rCN/strings.xml | 28 +- vector/src/main/res/values-zh-rTW/strings.xml | 20 +- vector/src/main/res/values/strings.xml | 7 +- 27 files changed, 2022 insertions(+), 786 deletions(-) diff --git a/matrix-sdk-android/src/main/res/values-ca/strings.xml b/matrix-sdk-android/src/main/res/values-ca/strings.xml index 4919f9f0..835ed38a 100644 --- a/matrix-sdk-android/src/main/res/values-ca/strings.xml +++ b/matrix-sdk-android/src/main/res/values-ca/strings.xml @@ -43,8 +43,8 @@ %1$s ha eliminat el nom de la sala %1$s ha eliminat el tema de la sala "ha redactat %1$s " - " per %1$s" - " [raó: %1$s]" + per %1$s + [raó: %1$s] %1$s ha actualitzat el seu perfil %2$s %1$s ha enviat una invitació a l\'usuari %2$s per a entrar a la sala %1$s ha acceptat la invitació per a %2$s diff --git a/matrix-sdk-android/src/main/res/values-de/strings.xml b/matrix-sdk-android/src/main/res/values-de/strings.xml index 3fc39446..f1d8fcbc 100644 --- a/matrix-sdk-android/src/main/res/values-de/strings.xml +++ b/matrix-sdk-android/src/main/res/values-de/strings.xml @@ -39,7 +39,7 @@ (Profilbild wurde ebenfalls geändert) %1$s hat den Raumnamen entfernt %1$s hat das Raum-Thema entfernt - "Verborgen %1$s " + %1$s verborgen durch %1$s [Grund: %1$s] %1$s hat das Benutzerprofil aktualisiert %2$s diff --git a/matrix-sdk-android/src/main/res/values-nl/strings.xml b/matrix-sdk-android/src/main/res/values-nl/strings.xml index ac8da688..f356c4e2 100644 --- a/matrix-sdk-android/src/main/res/values-nl/strings.xml +++ b/matrix-sdk-android/src/main/res/values-nl/strings.xml @@ -2,69 +2,69 @@ %1$s: %2$s - %1$s stuurde een afbeelding. + %1$s heeft een afbeelding gestuurd. - %s\'s uitnodiging - %1$s nodigde %2$s uit - %1$s heeft jou uitgenodigd - %1$s is tot de ruimte toegetreden - %1$s heeft de ruimte verlaten - %1$s heeft de uitnodiging niet geaccepteerd - %1$s verwijderde %2$s + Uitnodiging van %s + %1$s heeft %2$s uitgenodigd + %1$s heeft u uitgenodigd + %1$s neemt nu deel aan het gesprek + %1$s heeft het gesprek verlaten + %1$s heeft de uitnodiging geweigerd + %1$s heeft %2$s uit het gesprek verwijderd %1$s heeft %2$s ontbannen %1$s heeft %2$s verbannen - %1$s heeft de uitnodiging van %2$s teruggetrokken - %1$s heeft zijn of haar avatar aangepast - %1$s heeft zijn of haar naam aangepast naar %2$s - %1$s heeft zijn of haar naam aangepast van %2$s naar %3$s - %1$s heeft zijn of haar naam verwijderd (%2$s) + %1$s heeft de uitnodiging van %2$s ingetrokken + %1$s heeft zijn/haar avatar aangepast + %1$s heeft zijn/haar naam aangepast naar %2$s + %1$s heeft zijn/haar naam aangepast van %2$s naar %3$s + %1$s heeft zijn/haar naam verwijderd (%2$s) %1$s heeft het onderwerp veranderd naar: %2$s - %1$s heeft de ruimtenaam veranderd naar: %2$s - %s heeft een video-oproep geplaatst. - %s heeft een spraak-oproep geplaatst. + %1$s heeft de gespreksnaam veranderd naar: %2$s + %s heeft een video-oproep gemaakt. + %s heeft een spraakoproep gemaakt. %s heeft de oproep beantwoord. - %s heeft de oproep beëindigd. - %1$s heeft de toekomstige geschiedenis beschikbaar gemaakt voor %2$s - alle ruimte deelnemers, vanaf het punt dat ze zijn uitgenodigd. - alle ruimte deelnemers, vanaf het punt dat ze zijn toegetreden. - alle ruimte deelnemers. - Iedereen. + %s heeft opgehangen. + %1$s heeft de toekomstige gespreksgeschiedenis zichtbaar gemaakt voor %2$s + alle deelnemers aan het gesprek, vanaf het punt dat ze zijn uitgenodigd. + alle deelnemers aan het gesprek, vanaf het punt dat ze zijn toegetreden. + alle deelnemers aan het gesprek. + iedereen. onbekend (%s). - %1$s heeft eind-tot-eind encryptie aangezet (%2$s) + %1$s heeft eind-tot-eind-versleuteling aangezet (%2$s) - %1$s heeft een VoIP vergadering aangevraagd - VoIP vergadering gestart - VoIP vergadering gestopt + %1$s heeft een VoIP-vergadering aangevraagd + VoIP-vergadering gestart + VoIP-vergadering gestopt - (avatar was veranderd naar) - %1$s heeft de ruimtenaam verwijderd - %1$s heeft het ruimteonderwerp verwijderd - verdwijderd %1$s + (avatar is ook veranderd) + %1$s heeft de gespreksnaam verwijderd + %1$s heeft het gespreksonderwerp verwijderd + heeft %1$s verwijderd door %1$s [reden: %1$s] - %1$s heeft zijn of haar profiel %2$s geüpdatet - %1$s stuurde een uitnodiging naar %2$s om de ruimte toe te treden - %1$s accepteerde de uitnodiging voor %2$s + %1$s heeft zijn/haar profiel %2$s bijgewerkt + %1$s heeft een uitnodiging naar %2$s gestuurd om het gesprek toe te treden + %1$s heeft de uitnodiging voor %2$s aanvaard - ** Niet in staat tot het decoderen van: %s ** - De afzender\'s apparaat heeft geen sleutels voor dit bericht gestuurd. + ** Kan niet ontsleutelen: %s ** + Het apparaat van de afzender heeft geen sleutels voor dit bericht gestuurd. Kon niet verwijderd worden - Niet in staat om het bericht te sturen + Kan bericht niet verzenden Uploaden van de afbeelding mislukt Netwerkfout - Matrix fout + Matrix-fout - Het is momenteel niet mogelijk om een lege ruimte opnieuw toe te treden. + Het is momenteel niet mogelijk om een lege kamer opnieuw toe te treden. Versleuteld bericht @@ -76,16 +76,16 @@ Als antwoord op - verstuurde een plaatje. - verstuurde een video. - verstuurde een audiobestand. - verstuurde een bestand. + heeft een afbeelding gestuurd. + heeft een video gestuurd. + heeft een audiobestand gestuurd. + heeft een bestand gestuurd. Uitnodiging van %s - Ruimte uitnodiging + Gespreksuitnodiging %1$s en %2$s - Lege ruimte + Lege kamer %1$s en 1 andere diff --git a/matrix-sdk-android/src/main/res/values-pl/strings.xml b/matrix-sdk-android/src/main/res/values-pl/strings.xml index 7cc0dd2c..b6194feb 100644 --- a/matrix-sdk-android/src/main/res/values-pl/strings.xml +++ b/matrix-sdk-android/src/main/res/values-pl/strings.xml @@ -35,7 +35,7 @@ %s zakończył(a) rozmowę. %1$s usunął(-ęła) nazwę pokoju %1$s usunął(-ęła) temat pokoju - " [powód: %1$s]" + [powód: %1$s] %1$s wysłał(a) naklejkę. %1$s włączył(a) szyfrowanie end-to-end (%2$s) @@ -54,7 +54,7 @@ %1$s i jeden inny %1$s i kilku innych %1$s i %2$d innych - + ** Nie można odszyfrować: %s ** @@ -68,8 +68,8 @@ Rozpoczęto grupowe połączenie głosowe VoIP Zakończono grupowe połączenie głosowe VoIP - "zredagowane %1$s… " - " przez %1$s" + zredagowane %1$s + przez %1$s %1$s zaktualizował swój profil %2$s %1$s wysłał(a) zaproszenie do %2$s aby dołączył(a) do tego pokoju %1$s zaakceptował(a) zaproszenie dla %2$s diff --git a/matrix-sdk-android/src/main/res/values-sk/strings.xml b/matrix-sdk-android/src/main/res/values-sk/strings.xml index c1dbf873..dabc86f7 100644 --- a/matrix-sdk-android/src/main/res/values-sk/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sk/strings.xml @@ -38,9 +38,9 @@ (a tiež obrázok v profile) %1$s odstránil názov miestnosti %1$s odstránil tému miestnosti - "zmazaná udalosť %1$s " - " používateľom %1$s" - " [dôvod: %1$s]" + "vymazané %1$s " + používateľom %1$s + [dôvod: %1$s] %1$s aktualizoval svoj profil %2$s %1$s pozval %2$s vstúpiť do miestnosti %1$s prijal pozvanie do %2$s @@ -81,7 +81,7 @@ %1$s a 1 ďalší %1$s a %2$d ďalší %1$s a %2$d ďalších - + diff --git a/matrix-sdk-android/src/main/res/values-sq/strings.xml b/matrix-sdk-android/src/main/res/values-sq/strings.xml index 77d31a43..7231a7ce 100644 --- a/matrix-sdk-android/src/main/res/values-sq/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml @@ -28,8 +28,8 @@ (u ndryshua edhe avatari) %1$s hoqi emrin e dhomës - " nga %1$s" - " [arsye: %1$s]" + nga %1$s + [arsye: %1$s] %1$s përditësoi profilin e tij %2$s %1$s pranoi ftesën tuaj për %2$s @@ -41,7 +41,7 @@ S’u redaktua dot S’arrihet të dërgohet mesazh - S’u arrit të ngarkohej figurë + Ngarkimi i figurës dështoi Gabim rrjeti Gabim Matrix @@ -76,7 +76,7 @@ %1$s aktivizoi fshehtëzim skaj-më-skaj (%2$s) %1$s hoqi temën e dhomës - "redaktoi %1$s " + redaktoi %1$s %1$s dërgoi një ftesë për %2$s që të marrë pjesë në dhomë %1$s dhe 1 tjetër diff --git a/vector/src/main/res/values-bg/strings.xml b/vector/src/main/res/values-bg/strings.xml index e2762381..8f1840d2 100644 --- a/vector/src/main/res/values-bg/strings.xml +++ b/vector/src/main/res/values-bg/strings.xml @@ -1134,7 +1134,7 @@ Не беше открит валиден APK пакет за Google Play услугите. Уведомленията може да не работят правилно. - Riot.im - комуникирайте по свой начин. + Riot.im - комуникирайте по свой начин Универсален и сигурен чат изцяло под ваш контрол. "Чат приложение, което е изцяло гъвкаво и под Ваш контрол. Riot позволява да комуникирате по начина, по който искате. Направено за [matrix] - стандарт за отворена и децентрализирана комуникация. @@ -1362,4 +1362,30 @@ \n%s Използвай конфигурацията + Инициализиране на услугата + Медия + Компресия по подразбиране + Избери + Източник на медия по подразбиране + Избери + Издавай звук при снимане + + Маркирай като прочетено + Приложението няма нужда да се свързва със сървъра във фонов режим, което трябва да намали използването на енергия + + %1$s: 1 съобщение + %1$s: %2$d съобщения + + + %d известие + %d известия + + + Ново събитие + Стая + Нови съобщения + Нова покана + Аз + ** Неуспешно изпращане - моля, отворете стаята + diff --git a/vector/src/main/res/values-bn-rIN/strings.xml b/vector/src/main/res/values-bn-rIN/strings.xml index 5870bf29..126a5deb 100644 --- a/vector/src/main/res/values-bn-rIN/strings.xml +++ b/vector/src/main/res/values-bn-rIN/strings.xml @@ -268,9 +268,9 @@ এই ব্যাবহারকারী আগেও নাম ব্যবহার করে ছিল এই ইমেইল লিংক যেটা এখনো ক্লিক করা হয়নি - "আপনার প্রয়জনীয় পিছনে লগ করা যন্ত্রের জন্য এনক্রিপশন চাবি উৎপন্ন করতে শেষ থেকে শেষে এবং জমা করা জনগণ এর চাবি আপনার ঘরের লোক এর কাছে। -\nএটাই হলো একবার। -\nক্ষমা করবেন এটাই হলো অসুবিধা। " + আপনার প্রয়জনীয় পিছনে লগ করা যন্ত্রের জন্য এনক্রিপশন চাবি উৎপন্ন করতে শেষ থেকে শেষে এবং জমা করা জনগণ এর চাবি আপনার ঘরের লোক এর কাছে। +\nএটাই হলো একবার। +\nক্ষমা করবেন এটাই হলো অসুবিধা। <ও>পুনরায় অনুরোধ এনক্রিপশন চাবিগুলিআপনার অন্য যন্ত্রগুলি থেকে। @@ -332,12 +332,338 @@ তথা Riot এর প্রয়াজন অনুমতি নিতে আপনার ছবি এবং দৃশ্য রেকর্ড কে গ্রন্থাগার থেকে পাঠিয়ে জমার জায়গায় সংযুক্ত করতে। \nদেয়া করে অনুমতি দিন প্রবেশ করতে পরের pop-up কে যেটা সক্ষম আপনার নথি কে আপনার ফোন থেকে পাঠাতে। - "Riot এর প্রয়োজন অনুমতি নিয়ে প্রবেশে করতে আপনার ছবি তোলার যন্ত্র থেকে ছবি নিতে এবং দৃষ্টি রেকর্ড ডাকতে। " + Riot এর প্রয়োজন অনুমতি নিয়ে প্রবেশে করতে আপনার ছবি তোলার যন্ত্র থেকে ছবি নিতে এবং দৃষ্টি রেকর্ড ডাকতে। " \n -\nদয়াকরে অনুমতি দিন প্রবেশ করাতে পরের pop-pu কে যেটা ডাকতে সক্ষম" - "Riot এর প্রয়োজন অনুমতি নিয়ে প্রবেশ করতে আপনার মাইক্রোফোন আর মাধ্যমে শোনার কালএর সঞ্চালনা করতে " +\nদয়াকরে অনুমতি দিন প্রবেশ করাতে পরের পপ -আপ কে যেটা ডাকতে সক্ষম।" + Riot এর প্রয়োজন অনুমতি নিয়ে প্রবেশ করতে আপনার মাইক্রোফোন আর মাধ্যমে শোনার কালএর সঞ্চালনা করতে। " \n -\nদেয়া করে অনুমতিদিন প্রবেশ করতে পরের pop-up এ যেটা কল করতে সক্ষম" +\nদেয়া করে অনুমতিদিন প্রবেশ করতে পরের pop-up এ যেটা কল করতে সক্ষম।" + সেবা আরম্ভ করা হচ্ছে + ভিডিও কল সম্পাদনের জন্য Riot আপনার ক্যামেরা এবং আপনার মাইক্রোফোন অ্যাক্সেস করার অনুমতির প্রয়োজন। +\n +\nকল করতে সক্ষম হতে পরবর্তী পপ আপ অ্যাক্সেস অনুমতি দিন। + Riot আপনার ইমেল এবং ফোন নম্বরগুলির উপর ভিত্তি করে অন্যান্য ম্যাট্রিক্স ব্যবহারকারীদের খুঁজে পেতে আপনার ঠিকানা বই পরিচিতিগুলি অ্যাক্সেস করার অনুমতির প্রয়োজন। +\n +\nদয়া করে পরবর্তী পপ-আপ অ্যাক্সেসের অনুমতি দিয়ে ঠিকানা বই ব্যবহারকারীদের Riot থেকে অ্যাক্সেসযোগ্য করুন। + Riot কে আপনার ইমেল এবং ফোন নম্বরগুলির উপর ভিত্তি করে অন্যান্য ম্যাট্রিক্স ব্যবহারকারীদের সন্ধান করার জন্য আপনার ঠিকানা বই যোগাযোগ অ্যাক্সেস করার অনুমতির প্রয়োজন। +\n +\nRiot কে আপনার পরিচিতি অ্যাক্সেস করার অনুমতি দেবেন \? + + "দুঃখিত। কর্ম সঞ্চালিত না, অনুপস্থিত অনুমতির জন্য " + + সংরক্ষিত + ডাউনলোডে সংরক্ষণ করবেন\? + হ্যাঁ + না + প্রলম্বিত + + অপসারণ + যোগদান + প্রিভিউ + প্রত্যাখ্যান + + সদস্যদের তালিকা + হেডার খোলো + সিঙ্ক করা হচ্ছে… + প্রথম অপঠিত বার্তা তে ঝাঁপ দাও। + + আপনি এই রুমে %s দ্বারা যোগ দিতে আমন্ত্রিত হয়েছেন + এই আমন্ত্রণটি %s কে পাঠানো হয়েছিল, যা এই অ্যাকাউন্টের সাথে যুক্ত নয়। +\nআপনি একটি পৃথক অ্যাকাউন্ট দিয়ে লগইন করতে চান, অথবা আপনার অ্যাকাউন্টে এই ইমেল যোগ করতে পারেন। + আপনি %s অ্যাক্সেস করার চেষ্টা করছেন। আপনি কি আলোচনায় অংশ নিতে চান\? + একটা রুম + এটি এই রুমে একটি পূর্বরূপ। রুম মিথস্ক্রিয়া নিষ্ক্রিয় করা হয়েছে। + + নতুন বার্তা + সদস্য যোগ করুন + + ১ সক্রিয় সদস্য + %d সক্রিয় সদস্য + + + ১ জন সদস্য + "%d জন সদস্য" + + ১ জন সদস্য + + + ১ সেকেন্ড + %d সেকেন্ড + + + ১ মিনিট + %d মিনিট + + + ১ ঘন্টা + %d ঘন্টা + + + ১ দিন + %d দিন + + + রুম ছেড়ে দিন + আপনি কি রুম ছেড়ে যেতে চান\? + আপনি কি এই চ্যাট থেকে %s মুছে ফেলতে চান\? + সৃষ্টি + + অনলাইন + অফলিনে + অলস + এখুন %1$s + %1$s %2$s পূর্বে + + অ্যাডমিন সরঞ্জাম + কল + সরাসরি বার্তা + যন্ত্রগুলি + + আমন্ত্রণ + এই রুম ছাড়ো + এই রুমে থেকে সরান + নিষেধাজ্ঞা + নিষেধাজ্ঞা মুক্ত + পদাঘাত + স্বাভাবিক ব্যবহারকারী তে রিসেট করুন + মডারেটর কর + অ্যাডমিন কর + এই ব্যবহারকারীর কাছ থেকে সব বার্তা লুকান + এই ব্যবহারকারীর সব বার্তা দেখান + এই ব্যবহারকারীর কাছ থেকে সব বার্তা দেখান\? +\n +\nমনে রাখবেন যে এই পদক্ষেপটি অ্যাপ্লিকেশনটি পুনরায় চালু করবে এবং এটি কিছু সময় নিতে পারে। + ব্যবহারকারী আইডি, নাম বা ইমেইল + উল্লেখ + ডিভাইস তালিকা প্রদর্শন কর + আপনি এই পরিবর্তনটি পূর্বাবস্থায় ফিরিয়ে আনতে সক্ষম হবেন না যেহেতু আপনি ব্যবহারকারীকে একই শক্তি স্তর হিসাবে প্রচার করার জন্য প্রচার করছেন। +\nআপনি কি নিশ্চিত\? + + + আপনি কি নিশ্চিত যে এই চ্যাট থেকে এই ব্যবহারকারী কে পদাঘাত করতে চান\? + আপনি কি নিশ্চিত যে এই চ্যাট থেকে এই ব্যবহারকারীদের কে পদাঘাত করতে চান\? + + আপনি কি এই চ্যাট থেকে এই ব্যবহারকারীকে নিষিদ্ধ করতে চান\? + কারণ + + আপনি এই চ্যাটে %s কে আমন্ত্রণ জানাতে চান\? + "%1$s, " + %1$s আর %2$s + %1$s %2$s + + আইডি দ্বারা আমন্ত্রিত + স্থানীয় যোগাযোগ (%d) + ব্যবহারকারী নির্দেশিকা (%s) + শুধুমাত্র ম্যাট্রিক্স ব্যবহারকারীরা + + আইডি দ্বারা ব্যবহারকারী আমন্ত্রণ কর + এক বা একাধিক ইমেইল ঠিকানা বা ম্যাট্রিক্স আইডি লিখুন + ইমেইল বা ম্যাট্রিক্স আইডি + + অনুসন্ধান + %s টাইপ করছেন… + %1$s আর %2$s টাইপ করছেন… + %1$s আর %2$s এবং অন্যরা টাইপ করছেন… + একটি এনক্রিপ্ট করা বার্তা পাঠান… + একটি বার্তা পাঠান (এনক্রিপশনবিহীন)… + একটি এনক্রিপ্ট করা উত্তর পাঠান… + একটি উত্তর পাঠান (এনক্রিপশনবিহীন)… + সার্ভারের সাথে সংযুক্তি হারিয়ে গেছে। + বার্তা পাঠানো হয় নি। %1$s বা %2$s এখন\? + অজানা ডিভাইস উপস্থিত থাকার কারণে বার্তা পাঠানো হয় নি। %1$s বা %2$s এখন\? + সব আবার পাঠান + সব বাতিল করুন + অপ্রচলিত বার্তা আবার পাঠান + অপ্রত্যাশিত বার্তা মুছে দিন + ফাইল খুঁজে পাওয়া যায় নি + আপনার এই রুমে পোস্ট করার অনুমতি নেই + + ১টি নতুন বার্তা + %d টি নতুন বার্তা + + + আস্থাস্থাপন + বিশ্বাস করবেন না + লগআউট + উপেক্ষাকরা + আঙুলেরছাপ(%s): + রিমোট সার্ভারএর পরিচয় যাচাই করা হয়নি। + "এর অর্থ এই যে কেউ দূষিতভাবে আপনার ট্রাফিক কে আটকাতে পারে অথবা আপনার ফোন রিমোট সার্ভার দ্বারা সর্বরাহিত শংসাপত্রের উপর বিশ্বাস করে না." + যদি সার্ভার এডমিনিস্ট্রেটর বলে থাকেন যে এটি প্রত্যাশিত,নিচের আঙুলের ছাপ প্রদত্ত আঙুলের ছাপ মেলে নিশ্চিত করুন। + শংসাপত্র আপনার ফোন দ্বারা বিশ্বাস করা হয় যে এক থেকে পরিবর্তীত হয়েছে। এটি অত্যন্ত অস্বাভাবিক। আপনি এই নতুন শংসাপত্র গ্রহণ না করা বাঞ্চনীয়। + "সার্টিফিকেটটি পূর্বে বিশ্বাসযোগ্য এক থেকে একে বিশ্বস্ত নয়.সার্ভারটি এটির শংসাপত্র পুনর্বিকরণ করেছে।অনুমোদিত আঙুলের ছাপ এর জন্য সার্ভার প্রশাসকের সাথে যোগাযোগ করুন।" + কেবলমাত্র সার্ভার প্রশাসকটি উপরের মেলে এমন একটি আঙুলের ছাপ প্রকাশ করলেই শংসাপত্রটি গ্রহণ করবে। + + ঘরের বিস্তারিত + লোকজন + নথিগুলি + নির্ধারণ + + %d নির্বাচিত + + + বিকৃত পরিচয়।একটি ইমেইল ঠিকানা বা একটি মাধ্যমিক পরিচয় হতে হবে যেমন \'@localpart:domain\' + আমন্ত্রিত + যোগকরা + + বিষয়বস্তুর বিবরণী দেয়ার জন কারণ + ব্যাবহারকারির কাছ থেকে আপনি কি সব বার্তা লোকাতে চান\?উল্লেখ্য এই প্রতিক্রিয়াটি চালু করবেন এবং এটি কিছু সময় নিতে পারে। + আপলোড বাতিল করুন + নামানো বাতিল করুন + + খোঁজা + "ছাঁকুন ঘরের সংখ্যাগুলো " + কোনো ফলাফল নেই + ঘর + বার্তাগুলি + লোকজন + নথি + + যোগদান + নির্দেশক + প্রিয় + ঘরগুলি + "কম গুরুত্ব " + আমন্ত্রণ + চ্যাট শুরু করুন + ঘর তৈরিকরা + যোগদান করুন ঘরে + যোগকরুন একটা ঘর + আদর্শ ঘরের পরিচয় অথবা একটা ঘর আর উপনাম + + ব্রাউস নির্দেশনা + + "একটা ঘর " + %d ঘরগুলি + + + %২$s এর জন্য ঘর খোঁজা হয়েছে %১$s + %২$s এর জন্য ঘরগুলি খোঁজা হয়েছে %১$s + + খোঁজা হচ্ছে নির্দেশনা… + + সব বার্তা (হইচই) + সব বার্তাগুলি + শুধুমাত্র উল্লেখ করুন + নিঃশব্দ + প্রিয় + অনাগ্রাধিকার + সরাসরি চ্যাট + ত্যাগ করুন কথোপকথন + ভুলেযাওয়া + ঘরের পর্দার ছোটোখাটো যোগ করুন + + বার্তা + নির্ধারণ + সংস্করণ + সংস্করণ %s + শর্তাবলী + তৃতীয় পক্ষের বিগপ্তি + গ্রন্থস্বত্ব + গোপনীয় পন্থা + + "নথির ছবি " + প্রদশনীয় নাম + ইমেইল + যোগ করুন ইমেইল এর ঠিকানা + ফোন + যোগ করুন ফোন নম্বর + আবেদন তথ্য + সিস্টেম সেটিংস অ্যাপ্লিকেশন তথ্য প্রদর্শন করুন। + + + উন্নত বিজ্ঞপ্তি সেটিংস + ইভেন্ট দ্বারা বিজ্ঞপ্তি সেট করুন, শব্দ, নেতৃত্বে, কম্পন কনফিগার করুন + ঘটনা দ্বারা বিজ্ঞপ্তির গুরুত্ব + + বিজ্ঞপ্তির গোপনীয়তা + বিজ্ঞপ্তিগুলি সমাধান করুন + ডায়াগনস্টিকস এর সমস্যা সমাধান + পরীক্ষাগুলি চালাও + "(%1$d of %2$d) চলছে… " + বিজ্ঞপ্তি আপনার অ্যাকাউন্টের জন্য নিষ্ক্রিয় করা হয়েছে। + সক্ষম + + যন্ত্র সেটিংস। + বিজ্ঞপ্তি এই ডিভাইসের জন্য সক্রিয় করা হয়েছে। + বিজ্ঞপ্তি এই ডিভাইসের জন্য অনুমতি দেওয়া হয় নি। +\nRiot এর সেটিংস চেক করুন। + সক্ষম + + কাস্টম সেটিংস। + লক্ষ্য করুন যে কিছু বার্তা টাইপ নীরব করা হয়েছে (কোন শব্দ ছাড়াই একটি বিজ্ঞপ্তি তৈরি করবে)। + কিছু বিজ্ঞপ্তি আপনার কাস্টম সেটিংস এ নিষ্ক্রিয় করা হয়েছে। + কাস্টম নিয়ম লোড করতে ব্যর্থ হয়েছে, আবার চেষ্টা করুন। + সেটিংস চেক করুন + + Play Services পরীক্ষা + গুগল প্লে সার্ভিসেস APK পাওয়া গেছে এবং আপ টু ডেট রয়েছে। + Riot পুশ বার্তার প্রদানের জন্য Google Play পরিষেবাদি ব্যবহার করে কিন্তু এটি সঠিকভাবে কনফিগার করা বলে মনে হচ্ছে না: +\n%1$s + Play Services ঠিক করুন + + Firebase এর টোকেন + FCM টোকেন সফলভাবে উদ্ধার করা হয়েছে: +\n%1$s + FCM টোকেন উদ্ধার করতে ব্যর্থ হয়েছে: +\n%1$s + [%1$s] +\nএই ত্রুটিটি Riot এর নিয়ন্ত্রণের বাইরে এবং Google এর মতে, এই ত্রুটিটি ইঙ্গিত করে যে ডিভাইসটিতে FCM এর সাথে নিবন্ধিত অনেকগুলি অ্যাপ্লিকেশন রয়েছে। ত্রুটিগুলি কেবলমাত্র অ্যাপ্লিকেশনের চরম সংখ্যাগুলিতে ঘটে থাকে, তাই এটি গড় ব্যবহারকারীকে প্রভাবিত করবে না। + [%1$s] +\nএই ত্রুটি Riot এর নিয়ন্ত্রণ বাইরে। এটা বিভিন্ন কারণে ঘটতে পারে। আপনি পরে পুনরায় চেষ্টা করলে হয়তো এটি কাজ করবে, আপনি এটিও পরীক্ষা করতে পারেন যে Google Play পরিষেবাটি সিস্টেম সেটিংসে ডেটা ব্যবহারের ক্ষেত্রে সীমাবদ্ধ নয়, অথবা আপনার ডিভাইসের ঘড়ি সঠিক, বা এটি কাস্টম রমতে ঘটতে পারে। + [%1$s] +\nএই ত্রুটি Riot এর নিয়ন্ত্রণের বাইরে। ফোনে কোন গুগল একাউন্ট নেই। অ্যাকাউন্ট ম্যানেজার খুলুন এবং একটি গুগল একাউন্ট যোগ করুন। + একাউন্ট যোগ করুন + + টোকেন নিবন্ধন + FCM টোকেন সফলভাবে হোম সার্ভারে নিবন্ধিত। + হোম সার্ভারে FCM টোকেন নিবন্ধন করতে ব্যর্থ হয়েছে: +\n%1$s + + বিজ্ঞপ্তিগুলির সেবা + বিজ্ঞপ্তিগুলির সেবা চলছে। + বিজ্ঞপ্তিগুলির সেবা চলছে না। +\nঅপ্প্লিকেশনটি পুনরায় আরম্ভ করার চেষ্টা করুন। + পরিসেবা আরম্ভ + + বিজ্ঞপ্তি পরিষেবা অটো-পুনরায়-আরম্ভ + সেবা টি হত্যা করা হয়েছে এবং স্বয়ংক্রিয়ভাবে পুনরায় আরম্ভ করা হয়েছে। + পরিষেবা পুনরায় আরম্ভ করতে ব্যর্থ হয়েছে + + বুট করার সময় শুরু + ডিভাইসটি পুনরায় চালু হলে পরিষেবা শুরু হবে। + ডিভাইসটি পুনরায় চালু হওয়ার সময় পরিষেবাটি শুরু হবে না, আপনি একবার Riot টি খোলা না হওয়া পর্যন্ত বিজ্ঞপ্তি পাবেন না। + বুট থেকে শুরু করা সক্রিয় করুন + + ব্যাকগ্রাউন্ড এর সীমাবদ্ধতা চেক করুন + ব্যাকগ্রউন্ডের সীমাবদ্ধতা Riot এর জন্য নিষ্ক্রিয় করা হয়েছে। এই পরীক্ষা মোবাইল ডেটা ব্যবহার করে চালানো উচিত (ওয়াইফাই না)। +\n%1$s + ব্যাকগ্রউন্ডের সীমাবদ্ধতা রিমোট এর জন্য সক্রিয় করা হয়েছে। +\nঅ্যাপ্লিকেশন যেটি করার চেষ্টা করে সেটি ব্যাকগ্রাউন্ডে থাকা অবস্থায় আক্রমনাত্মকভাবে সীমিত হবে এবং এটি বিজ্ঞপ্তিগুলিতে প্রভাবিত হতে পারে। +\n%1$s + সীমাবদ্ধগুলি নিষ্ক্রিয় + + ব্যাটারি অপ্টিমাইজেশান + Riot ব্যাটারি অপ্টিমাইজেশান দ্বারা প্রভাবিত হয় না। + যদি কোনও ব্যবহারকারী কোনও ডিভাইসটিকে নির্দিষ্ট সময়ের জন্য আনপ্লাগ এবং স্থিতিশীল রাখে তবে স্ক্রীন বন্ধের সাথে ডিভাইসটি ডোজ মোডে প্রবেশ করে। এটি অ্যাপ্লিকেশানগুলিকে নেটওয়ার্ক অ্যাক্সেস করতে বাধা দেয় এবং তাদের কাজ, সিঙ্ক এবং মান অ্যালার্মগুলি স্থগিত করে। + "অপ্টিমাইজেশান অবহেলা " + + সাধারণ + হ্রাস গোপনীয়তা + অ্যাপ্লিকেশন ব্যাকগ্রাউন্ডে চালানোর অনুমতি প্রয়োজন + সাধারণ লক্ষণ ঠিক আছে.যদি আপনি এখনও কোনো প্রজ্ঞাপন পাননি,দয়াকরে জমা করুন একটা গুরুত্বপূর্ণ খসড়া যেটা সাহায্য করবে তদন্ত করতে। + এক অথবা অধিক পরীক্ষা বার্থ হয়েছে,প্রস্তাবিত ঠিক করে চেষ্টা করুন(es). + এক অথবা অধিক পরীক্ষা ব্যর্থ হয়েছে,দয়া করে জমা করুন একটা গুরুত্বপূর্ণ খসড়া যেটা সাহায্য করবে অনুসন্ধান করতে। + + পদ্ধতি নির্ধারণ। + বিজ্ঞাপ্তিকে পদ্ধতি নির্ধারণ এর মাধ্যমে সক্রিয় করা হয়েছে। + বিজ্ঞাপ্তিকে পদ্ধতি নির্ধারণ এর মাধ্যমে নিষ্ক্রিয় করা হয়েছে। +\nদেয়া করে দেখে নিন পদ্ধতি নির্ধারণ। + খুলুন নির্ধারণটি + + গণনা নির্ধারণ। + বিজ্ঞাপ্তি আপনার একাউন্টএর জন্য সক্রিয় করা হোক. + পঠিত হিসেবে চিহ্নিত diff --git a/vector/src/main/res/values-ca/strings.xml b/vector/src/main/res/values-ca/strings.xml index 23a5fc75..a73b7991 100644 --- a/vector/src/main/res/values-ca/strings.xml +++ b/vector/src/main/res/values-ca/strings.xml @@ -1123,7 +1123,7 @@ A la pantalla següent se us demanarà que permeteu al Riot executar-se sempre a %d+ No s\'ha trobat cap APK de Google Play Services vàlid. Les notificacions poden no funcionar correctament. - Riot.im - Comunica\'t, a la teva manera. + Riot.im - Comunica\'t, a la teva manera "Sempre hi som fent canvis i millores al Riot.im. Podeu trobar el registre de canvis complet aquí: %1$s. Per assegurar-vos que no us perdeu res, només heu de mantenir les actualitzacions automàtiques habilitades." @@ -1382,4 +1382,25 @@ Per què triar Riot.im? S\'ha trobat una còpia de seguretat nova de la clau. \n \nSi no heu configurat el mètode de recuperació nou, un atacant podria estar intentant accedir al vostre compte. Canvieu la contrasenya del vostre compte i configureu-hi un mètode de recuperació nou immediatament a la configuració. + Inicialitzar servei + Ignorar + + Registrar-se amb Single Sign-on + Aquesta URL no està disponible , si us plau verifiqueu-la + El vostre dispositiu està usant una versió obsoleta del protocol de seguretat TLS, vulnerable a atacs. Per a la vostra seguretat no us podreu connectar + Envieu un missatge amb Enter + La tecla Enter del teclat virtual enviarà un missatge en comptes d\'afegir un salt de línia + + Actualitzar la contrasenya + La contrasenya no és vàlida + Les contrasenyes no coincideixen + + Medis + Compressió estàndard + Escollir + Origen de medis per defecte + Escollir + Resposta no vàlida en descobrir homeservers + Usar Config + diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index 86182d5b..5099c90e 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -509,9 +509,9 @@ Um fortzufahren, gib dein Passwort ein. Diese Telefonnummer wird bereits verwendet. Passwort ändern - Altes Passwort + Aktuelles Passwort Neues Passwort - Passwort bestätigen + Neues Passwort bestätigen Ändern des Passworts fehlgeschlagen Dein Passwort wurde aktualisiert Alle Nachrichten von %s anzeigen? @@ -901,7 +901,7 @@ Unbekannte Geräte: Normal Verringerter Datenschutz Dies App braucht die Berechtigung im Hintergrund zu laufen - • Benachrichtigungen werden über Google Cloud Messaging versendet + • Benachrichtigungen werden über Firebase Cloud Messaging versendet • Benachrichtigungen enthalten nur Metadaten • Der Nachrichteninhalt der Benachrichtigung wird sicher vom Matrix-Heimserver abgerufen • Benachrichtigungen enthalten Metadaten und Nachrichteninhalte @@ -933,10 +933,10 @@ Willst du welche hinzufügen? Jetzt prüfen Account deaktiveren - Dies wir dein Konto permanent unbenutzbar machen. Du wirst dich nicht anmelden können und keiner wird den selben Nutzernamen erneut registrieren können. Dies wird dein Konto aus allen Räumen austreten lassen, an denen es teilnimmt und es wird deine Kontoangaben vom Identitätsserver löschen. Diese Aktion ist unumkehrbar. -\n -\nDie Deaktivierung deines Konto wird standardmäßig keine deiner gesendeten Nachrichten löschen. Wenn du möchtest, dass auch deine Nachrichten gelöscht werden, wähle zusätzlich die untere Option. -\n + Dies wir dein Konto permanent unbenutzbar machen. Du wirst dich nicht anmelden können und keiner wird denselben Nutzernamen erneut registrieren können. Dies wird dein Konto aus allen Räumen austreten lassen, an denen es teilnimmt und es wird deine Kontoangaben vom Identitätsserver löschen. Diese Aktion ist unumkehrbar. +\n +\nDie Deaktivierung deines Konto wird standardmäßig keine deiner gesendeten Nachrichten löschen. Wenn du möchtest, dass auch deine Nachrichten gelöscht werden, wähle zusätzlich die untere Option. +\n \nDie Sichtbarkeit deiner Nachrichten ist ähnlich wie bei E-Mails: Wenn deine Nachrichten gelöscht werden, bedeutet dies, dass von dir verschickte Nachrichten nicht mit neuen oder unregistrierten Nutzern geteilt werden. Aber registrierte Nutzer, die bereits Zugang zu diesen Nachrichten haben, behalten weiterhin Zugriff auf ihr Exemplar. Bitte alle Nachrichten, die ich gesendet habe, löschen, wenn mein Account deaktiviert wird (Warnung: Unterhaltungen werden für zukünftige Nutzer unvollständig erscheinen) Um fortzufahren, bitte Password eingeben: @@ -985,7 +985,7 @@ Willst du welche hinzufügen? Kickt Benutzer mit angegebener ID Ändert deinen Anzeigenamen (De-)Aktiviert Markdown - Setzt Matrix-App-Token zurück + Um das Matrix-App-Management zu reparieren Dieser Raum wurde ersetzt und ist nicht länger aktiv Die Konversation wird hier fortgesetzt @@ -1182,7 +1182,7 @@ Versuche die Anwendung neuzustarten. Prüfung der Play-Dienste Google Play-Dienste-APK ist verfügbar und aktuell. Token-Registrierung - "Wenn ein Benutzer ein abgestecktes Gerät mit ausgeschaltetem Bildschirm eine Weile nicht bewegt, wechselt es in den Doze-Modus. Dies hindert Apps daran, auf das Netzwerk zuzugreifen und verzögert die Ausführung von Aufgaben, Synchronisierungen und Standard-Alarmen. " + Wenn ein Benutzer ein abgestecktes Gerät mit ausgeschaltetem Bildschirm eine Weile nicht bewegt, wechselt es in den Doze-Modus. Dies hindert Apps daran, auf das Netzwerk zuzugreifen und verzögert die Ausführung von Aufgaben, Synchronisierungen und Standard-Alarmen. Ignoriere Optimierungen Hintergrundverbindung @@ -1197,7 +1197,7 @@ Versuche die Anwendung neuzustarten. Keine validen Google-Play-Dienste gefunden. Benachrichtigungen könnten nicht richtig funktionieren. - Riot.im - Kommunizierte auf deine Weise. + Riot.im - Kommunizierte auf deine Weise Eine universelle, sichere Chat-App - komplett unter deiner Kontrolle. "Eine Chat-App unter deiner Kontrolle und total flexibel. Riot lässt dich auf die Art kommunizieren wie du willst. Die App wurde gemacht für [matrix] - dem Standard für offene, dezentrale Komunikation. @@ -1341,7 +1341,7 @@ Warnung: Diese Datei wird gelöscht wenn die Anwendung deinstalliert wird.Konnte Vertrauensinformationen für die Sicherung nicht abrufen (%s). Um Sichere Nachrichtenwiederherstellung auf diesem Gerät zu nutzen, gebe jetzt deine Wiederherstellungspassphrase oder -schlüssel ein. - Deine gesicherten Verschlüsselungsschlüssel vom Server löschen? Du wirst deinen Wiederherstellungsschlüssel nicht mehr nutzen um deinen verschlüsselten Chatverlauf zu lesen. + Deine gesicherten Verschlüsselungsschlüssel vom Server löschen\? Du wirst deinen Wiederherstellungsschlüssel nicht mehr nutzen können, um deinen verschlüsselten Chatverlauf zu lesen. Beim Ausloggen gehen ihre verschlüsselten Nachrichten verloren Schlüssel-Sicherung wird durchgeführt. Wenn sie sich jetzt ausloggen, dann gehen ihre verschlüsselten Nachrichten verloren. @@ -1421,10 +1421,9 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Das Passwort ist ungültig Passwörter stimmen nicht überein - Ungültige Rückmeldung bei Heimserver-Entdeckung + Ungültige Antwort beim Entdecken des Heimservers Serveroptionen vervollständigen - Riot hat eine benutzerdefinierte Serverkonfiguration für die Domäne deines Benutzernamens gefunden -\n\"%s\": + Riot hat eine benutzerdefinierte Serverkonfiguration für die Domäne deines Benutzernamens gefunden \"%s\": \n%s Nutze Konfiguration @@ -1436,4 +1435,22 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Wählen Auslöseton abspielen + Als gelesen markieren + Die App muss nicht im Hintergrund zum Heimserver verbinden, dies sollte die Akkunutzung reduzieren + + %1$s: 1 Nachricht + %1$s: %2$d Nachrichten + + + %d Nachricht + %d Nachrichten + + + Neues Ereignis + Raum + Neue Nachrichten + Neue Einladung + Ich + ** Fehler beim Senden - bitte Raum öffnen + diff --git a/vector/src/main/res/values-eo/strings.xml b/vector/src/main/res/values-eo/strings.xml index 79cb8d40..05caa64e 100644 --- a/vector/src/main/res/values-eo/strings.xml +++ b/vector/src/main/res/values-eo/strings.xml @@ -6,7 +6,7 @@ Nigra haŭto Sinkroniganta - Atenti pri eventoj + Atentanta pri eventoj Laŭtaj sciigoj Silentaj sciigoj @@ -72,4 +72,25 @@ Fermi Sendi ĉifritan respondon… Sendi respondon (neĉifritan)… + Homoj + Babilejoj + Komunumoj + + Filtri nomojn de babilejoj + Filtri homojn + Filtri nomojn de babilejoj + Homoj + Dosieroj + Agordoj + HOMOJ + DOSIEROJ + + ALIĜI + Homoj + Babilejoj + Neniu uzantoj + + Babilejoj + Salti al unua nelegita mesaĝo. + diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml index 23713fe7..0c95ef39 100644 --- a/vector/src/main/res/values-es/strings.xml +++ b/vector/src/main/res/values-es/strings.xml @@ -32,9 +32,10 @@ Renombrar Reportar contenido Llamada activa - Llamada de conferencia en curso.\nUnirse con %1$s o %2$s. - voz - vídeo + Llamada de conferencia en curso. +\nUnirse con %1$s o %2$s + Voz + Vídeo No se puede iniciar la llamada, por favor inténtalo de nuevo más tarde Debido a que faltan permisos, pueden faltar algunas características… Necesitas permiso para invitar a iniciar una conferencia en esta sala @@ -69,10 +70,10 @@ Salas - Buscar salas - Buscar favoritos - Buscar personas - Buscar salas + Filtrar salas + Filtrar favoritos + Filtrar personas + Filtrar salas Invitaciones @@ -107,14 +108,14 @@ El informe de error ha sido enviado con éxito No se pudo enviar el informe de error (%s) Progreso (%s%%) - La aplicación falló en la última sesión. ¿Te gustaría enviar un informe de fallas? + La aplicación falló en la última sesión. ¿Te gustaría enviar un informe de error\? Enviar en Leído Unirse a la Sala Nombre de usuario - Registrar + Crear cuenta Iniciar sesión Cerrar sesión URL del Servidor Doméstico @@ -130,7 +131,7 @@ Iniciar sesión - Registrar + Crear cuenta Enviar Omitir Enviar Correo Electrónico de Restauración @@ -192,7 +193,7 @@ Se ha cerrado sesión en todos tus dispositivos y ya no recibirás notificacione No es posible registrarse: Error de red No es posible registrarse No es posible registrarse : falló la propiedad del correo electrónico - Ingresa una URL válida + Por favor introduce una URL válida Nombre de usuario/contraseña inválidos No se reconoció el código de acceso especificado @@ -446,7 +447,7 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Añadir dirección de correo electrónico Teléfono Añadir número telefónico - Pantalla de información del sistema de la aplicación + Mostrar la pantalla de información de la aplicación de los ajustes del sistema. Información de la aplicación Habilitar notificaciones para esta cuenta @@ -791,7 +792,7 @@ Dispositivos desconocidos: Salir Comunidades - Buscar comunidades + Filtrar comunidades Invitar Comunidades @@ -904,8 +905,8 @@ Dispositivos desconocidos: Enviar una pegatina Actualmente no tienes ningún paquete de pegatinas habilitado. - -¿Añadir algunos ahora? +\n +\n¿Añadir algunos ahora\? Desactivar Cuenta Avatar @@ -1075,13 +1076,13 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de colapsar llamar de cada manera - Acceptar + Aceptar - Por favor revisar y aceptar las reglas de este servidor doméstico: + Por favor revisa y acepta las reglas de este servidor doméstico: Llamadas - Usar Riot sonido normal por entrando llamadas - Sonido de entrando llamadas + Usar el tono de llamada normal de Riot para llamadas entrantes + Tono para llamadas entrantes Eligir sonido de llamadas: Llamada de video en proceso… @@ -1094,7 +1095,58 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Razón Diagnóstico de fallas - Diagnóstico de fallas - Inicia pruebas + Diagnóstico de errores + Iniciar pruebas Haciendo… (%1$d of %2$d) + Iniciando servicio + Copia de seguridad de la clave + Usar copia de seguridad de la clave + + La copia de seguridad de la clave no ha finalizado, por favor espere… + No quiero mis mensajes cifrados + Creando copia de seguridad de las claves… + Usar copia de seguridad de la clave + ¿Esta seguro\? + Copia de seguridad + Perderá el acceso a sus mensajes cifrados si cierra sesión sin hacer una copia de seguridad de sus claves. + + Quedarse + Saltar + Hecho + Cancelar + Ignorar + + Marcar como leído + Iniciar sesión con single-sign-on + Tu dispositivo usa una versión anticuada e insegura del protocolo de seguridad TLS. Por tu seguridad no puedes conectarte + Ajustes avanzados de notificaciones + Ajusta la importancia de las notificaciones por evento, configura el sonido, LED y vibración + Importancia de notificación por evento + + Ajustes de sistema. + Las notificaciones están activadas en los ajustes de sistema. + Las notificaciones están desactivadas en los ajustes del sistema. +\nPor favor comprueba los ajustes de sistema. + Abrir ajustes + + Ajustes de cuenta. + Las notificaciones están activadas para tu cuenta. + Las notificaciones están desactivadas para tu cuenta. +\nPor favor comprueba los ajustes de cuenta. + Activar + + Ajustes de dispositivo. + Las notificaciones están activadas para este dispositivo. + Las notificaciones no están permitidos para este dispositivo. +\nPor favor comprueba los ajustes Riot. + Activar + + Ajustes personalizados. + Ten en cuenta que algunos mensajes son silenciosos (producen una notificación sin sonido). + Algunas notificaciones están desactivadas en tus ajustes personalizados. + Error al cargar reglas personalizadas, por favor prueba de nuevo. + Comprueba ajustes + + Prueba de servicios Google Play + APK de servicios de Google Play esta disponible y actualizado. diff --git a/vector/src/main/res/values-eu/strings.xml b/vector/src/main/res/values-eu/strings.xml index b5407444..ad7a9629 100644 --- a/vector/src/main/res/values-eu/strings.xml +++ b/vector/src/main/res/values-eu/strings.xml @@ -849,7 +849,7 @@ Gailu ezezagunak: Arrunta Pribatutasun murriztua Aplikazioak bigarren planoan aritzeko baimenak behar ditu - • Jakinarazpenak Google Cloud Messaging bidez bidaltzen dira + • Jakinarazpenak Firebase Cloud Messaging bidez bidaltzen dira • Jakinarazpenek meta-datuak besterik ez dituzte • Jakinarazpen mezuaren edukia Matrix hasiera-zerbitzarian gorde da seguru • Jakinarazpenek mezuen datuak eta metadatuak dituzte @@ -1142,7 +1142,7 @@ Aplikazioa egiten saiatzen ari dena agresiboki murriztuko zaio bigarren planoan Ez da baliozko Google Play Services APK-rik aurkitu. Jakinarazpenak agian ez dira ongi ibiliko. - Riot.im - Komunikatu, zure erara. + Riot.im - Komunikatu, zure erara Txat seguru eta unibertsala zure kontrolpean erabat. Erabiltzaile batek gailu bat deskonektatuta eta erabili gabe uzten badu denbora batez, pantaila itzalita duela, gailua kuluxka moduan sartzen da. Honek aplikazioak sarera konektatzea eragozten du eta beraien lanak atzeratzen ditu, baita ohiko alarmak. Riot-ek bigarren planoko konexio arin bat behar du jakinarazpen fidagarriak izateko. @@ -1298,4 +1298,104 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. Erabili gakoen babes-kopia Ziur al zaude\? Babes-kopia + Zerbitzua hasieratzen + Zure mezu zifratuetara sarbidea galduko duzu ez baduzu gakoen babes-kopia egiten saioa amaitu aurretik. + + Geratu + Abortatu + Ezikusi + + Ziur saioa amaitu nahi duzula\? + Hasi saioa urrats batean + URL-a ez dago eskuragarri, egiaztatu mesedez + Zure gailuak zaharkitutako TLS segurtasun protokolo bat darabil, erasotu daitekeena, zure segurtasunerako ezin izango zara konektatu + Bidali mezua Sartu tekla sakatuta + Teklatuko Sartu teklak mezua bidaliko du lerro saltoa sartu ordez + + Datuak aurrezteko moduak presentzia eguneratzeak eta idazte-jakinarazpenak iragazten ditu. + + Eguneratu pasahitza + Pasahitza baliogabea da + Pasahitzak ez datoz bat + + Multimedia + Lehenetsitako konpresioa + Hautatu + Lehenetsitako multimediaren iturria + Hautatu + Egin kliskatze soinua + + Zifratutako mezuak berreskuratzea + Sartu erabiltzaile-izena. + Hasi gakoen babes-kopia erabiltzen + (Aurreratua) + Esportatu gakoak manualki + + Babestu zure babes-kopia pasaesaldi batekin. + Zure gakoen zifratutako kopia bat gordeko dugu zure hasiera-zerbitzarian. Babestu babes-kopia pasaesaldi batekin seguru mantentzeko. +\n +\nSegurtasun hobe baterako, pasaesaldi hau eta zure kontuaren pasahitza desberdinak izan beharko lirateke. + Babes-kopia sortzen + Edo, babestu zure babes-kopia berreskuratze gako batekin, eta toki seguruan gorde. + (Aurreratua) Ezarri berreskuratze gakoa + Ongi! + Zure gakoen babes-kopia egiten ari da. + Zure berreskuratze gakoa badaezpadakoa da, pasaesaldia ahazten baduzu zure zifratutako mezuak berreskuratzeko gakoa erabili dezakezu. +\nGorde zure berreskuratze gakoa toki oso seguruan, pasahitz kudeatzaile batean esaterako (edo gordailu kutxan) + Gorde zure berreskuratze gakoa toki oso seguruan, esaterako pasahitz kudeatzaile batean (edo gordailu kutxan) + Kopia bat egin dut + Partekatu + Berreskuratze gakoa kalkulatzen… + Gakoak deskargatzen… + Gakoak inportatzen… + Gakoen babes-kopia berria + Zifratutako mezuen gako babes-kopia berri bat antzeman da. +\n +\nEz baduzu zuk berreskuratze metodo berri bat ezarri, erasotzaile batek zure kontua atzitzeko saiakerak egiten egon daiteke. Aldatu zure kontuaren pasahitza eta ezarri berreskuratze metodo berri bat berehala ezarpenetan. + Ni izan naiz + Ez galdu inoiz zifratutako mezuak + Hasi gakoen babes-kopia egiten + + Ez galdu inoiz zifratutako mezuak + Erabili gakoen babes-kopia + + Zifratutako mezuen gako berriak + Kudeatu gakoen babes-kopian + + Gakoen babes-kopia egiten… + + Gako guztien babes-kopia egin da + + Gako %den babes-kopia egiten… + %d gakoen babes kopia egiten… + + + Bertsioa + Algoritmoa + Sinadura + + Baliogabeko hasiera-zerbitzari deskubritze erantzuna + Automatikoki osatu zerbitzariaren aukerak + "Riot-ek pertsonalizatutako zerbitzari konfigurazio bat antzeman du zure erabiltzaile id-arentzat \"%s\" domeinuan: +\n%s" + Erabili konfigurazioa + + Markatu irakurritako gisa + Aplikazioak ez du hasiera-zerbitzarira bigarren planoan konektatzeko beharrik, bateria aurreztu beharko litzateke + + %1$s: mezu 1 + %1$s: %2$d mezu + + + jakinarazpen %d + %d jakinarazpen + + + Gertaera berria + Gela + Mezu berriak + Gonbidapen berria + Ni + ** Bidalketak huts egin du, ireki gela + diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index f47d29ea..309c6f81 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -92,7 +92,7 @@ Huoneet - Huonehakemisto + Huoneluettelo Ei huoneita Ei julkisia huoneita saatavilla @@ -183,7 +183,7 @@ Anna tilisi sähköpostiosoite. Anna uusi salasana. Osoitteeseen %s on lähetetty sähköposti. Sen jälkeen, kun olet seurannut siinä olevaa linkkiä, paina alla olevaa nappia. - Ei voitu vahvistaa sähköpostiosoitetta, seuraa linkkiä sähköpostissa jonka sait + Sähköpostiosoitteesi vahvitaminen epäonnistui. Varmistathan, että seurasit sähköpostissasi olevaa linkkiä Salasanasi on vaihdettu.\n\nSinut on kirjauduttu ulos kaikista laitteistasi, etkä enää saa viesti-ilmoituksia. Ottaaksesi käyttöön ilmoitukset uudelleen, kirjaudu sisään uudelleen kaikilla laitteillasi. @@ -365,7 +365,7 @@ Salli Riotin käyttää yhteystietojasi? Kirjaudu ulos Jätä huomiotta Sormenjälki (%s): - Palvelimen identiteettiä ei voitu varmistaa. + Palvelimen identiteettiä ei voitu vahvistaa. Tämä voi tarkoittaa että joku yrittää kaapata sinun viestintääsi tai että laitteesi ei luota palvelimen varmenteeseen. Jos palvelimen ylläpitäjä sanoo että tämä on odotettua, varmista että alla oleva sormenjälki on sama kuin hänen antama. Sertifikaatti johon laitteesi luotti aikaisemmin on vaihtunut. Tämä on HYVIN EPÄTAVALLISTA. Suositellaan että ET hyväksy tätä uutta sertifikaattia. @@ -413,7 +413,7 @@ Salli Riotin käyttää yhteystietojasi? Syötä huonetunniste tai -alias - Selaa hakemistoa + Selaa luetteloa Haetaan luettelosta… @@ -481,8 +481,8 @@ Salli Riotin käyttää yhteystietojasi? Oikeus yhteystietoihin Yhteystiedot (maa) Koti - Pin rooms with missed notifications - Pin rooms with unread messages + Kiinnitä huoneet, joissa on huomaamatta jääneitä ilmoituksia + Kiinnitä huoneet, joissa on lukemattomia viestejä Laitteet Laitteen tiedot ID @@ -527,7 +527,7 @@ Salli Riotin käyttää yhteystietojasi? Valitse maa Puhelinnumero Väärän muotoinen puhelinnumero valitulle maalle - Puhelinvarmennus + Puhelinnumeron varmennus Lähetimme aktivointikoodin tekstiviestinä. Syötä aktivointikoodi alapuolelle. Anna aktivointikoodi Puhelinnumeron aktivointi epäonnistui @@ -659,7 +659,7 @@ Salli Riotin käyttää yhteystietojasi? Vahvistaaksesi että tähän laitteeseen voi luottaa, ota yhteyttä sen omistajaan jollain muulla tavalla (esimerkiksi soittamalla tai tapaamalla) ja varmista että hänen laitteen avain on sama kuin alla oleva: Jos avain täsmää, paina painiketta “vahvista”. Jos avain ei täsmää, se tarkoittaa, että jokin tuntematon osapuoli lukee keskustelujanne. Tässä tapauksessa paina painiketta “kiellä”. \nTulevaisuudessa tämä varmennusprosessi tulee olemaan hienostuneempi. - Vahvistan että avaimet täsmäävät + Vahvistan, että avaimet täsmäävät Riot tukee viestien salausta. Kirjaudu sisään uudelleen ottaaksesi salaus käyttöön. @@ -670,7 +670,7 @@ Voit tehdä sen nyt tai myöhemmin sovelluksen asetuksissa. Huoneessa on tuntemattomia laitteita joita ei ole vahvistettu.\nLaitteet eivät välttämättä kuulu väitetyille omistajilleen.\nJokainen uusi laite kannattaa vahvistaa ennen kuin jatkat, mutta voit myös lähettää viestit vahvistamattomille laitteille.\n\nTuntemattomat laitteet: - Valitse huonehakemisto + Valitse huoneluettelo Palvelin saattaa olla tavoittamattomissa tai ylikuormitettu Syötä kotipalvelin jolta listataan julkiset huoneet Kotipalvelimen URL @@ -679,7 +679,7 @@ Voit tehdä sen nyt tai myöhemmin sovelluksen asetuksissa. Etsi historiasta - Käyttäjähakemisto + Käyttäjäluettelo KÄYTTÄJÄHAKEMISTO (%s) Käynnistä automaattisesti Tyhjennä mediavälimuisti @@ -1086,7 +1086,7 @@ Haluatko lisätä paketteja? Normaali Heikentynyt yksityisyys Tämä sovellus tarvitsee oikeuden ajaakseen taustapalveluaan - • ilmoitukset lähetetään Google Cloud Messaging -palvelun kautta + • ilmoitukset lähetetään Firebase Cloud Messaging -palvelun kautta • ilmoitukset sisältävät vain metadataa • ilmoituksessa olevan viestin sisältö haetaan turvallisesti suoraan Matrix-kotipalvelimelta • ilmoitus sisältää metadataa ja itse viestin @@ -1218,7 +1218,7 @@ Haluatko lisätä paketteja? Tee %s jatkaaksesi palvelun käyttöä. Lataa huoneen käyttäjät laiskasti - Paranna suorituskykyä lataamalla huoneen jäsenet ensimmäisellä kerralla. + Paranna suorituskykyä lataamalla huoneen jäsenet vasta, kun huone näytetään ensimmäisen kerran. Kotipalvelimesi ei tue huoneen jäsenten laiskaa latausta. Yritä myöhemmin. Törmäsimme virheeseen @@ -1237,7 +1237,7 @@ Haluatko lisätä paketteja? %d+ Kelvollista Google Play Services APK:ta ei löytynyt. Ilmoitukset eivät ehkä toimi oikein. - Riot.im - Kommunikoi, sinun tavallasi. + Riot.im - Kommunikoi, sinun tavallasi "Olemme aina tekemässä muutoksia ja parannuksia Riot.im:ään. Täydellisen muutoslokin löydät täältä: %1$s. Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytössä." @@ -1413,4 +1413,30 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös \n%s Käytä asetuksia + Alustetaan palvelua + Media + Oletuksena oleva pakkauksen määrä + Valitse + Oletuksena oleva medialähde + Valitse + Toista sulkimen ääni + + Merkitse luetuksi + Sovelluksen ei tarvitse pitää yhteyttä kotipalvelimeen, jonka pitäisi vähentää akunkäyttöä + + %1$s: yksi viesti + %1$s: %2$d viestiä + + + yksi ilmoitus + %d ilmoitusta + + + Uusi tapahtuma + Huone + Uusia viestejä + Uusi kutsu + Minä + ** Lähetys epäonnistui — avaathan huoneen + diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index d7a8d2c5..3793810f 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -852,7 +852,7 @@ Appareils inconnus : Normal Confidentialité réduite L\'application a besoin de la permission de fonctionner en arrière-plan - • Les notifications sont envoyées via Google Cloud Messaging + • Les notifications sont envoyées via Firebase Cloud Messaging • Les notifications ne contiennent que des métadonnées • Le contenu du message de notification est issu directement du serveur d’accueil Matrix • Les notifications contiennent des métadonnées et des messages @@ -1149,7 +1149,7 @@ Sur l’écran suivant on vous demandera d’autoriser Riot à toujours fonction Aucun APK des services Google Play valide n’a été trouvé. Les notifications peuvent ne pas fonctionner correctement. - Riot.im - Communiquez, à votre façon. + Riot.im - Communiquez, à votre façon Une application de discussion sécurisée universelle que vous contrôlez. "Une application de discussion, que vous contrôlez et entièrement flexible. Riot vous laisse communiquer comme vous le souhaitez. Conçu pour [matrix], le standard pour les communications libres et décentralisées. @@ -1385,4 +1385,22 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Choisir Jouer le son de l’obturateur + Marquer comme lu + Les applications <b>n’</a>ont <b>pas</b> besoin d’être connectées au serveur d’accueil en arrière-plan, cela devrait diminuer l’utilisation de la batterienot need to connect to the HomeServer in the background, it should reduce battery usage + + %1$s : 1 message + %1$s : %2$d messages + + + %d notification + %d notifications + + + Nouvel évènement + Salon + Nouveaux messages + Nouvelle invitation + Moi + ** Échec de l’envoi − veuillez ouvrir le salon + diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index b08963ea..8df5156d 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -849,7 +849,7 @@ Ismeretlen eszközök: Címzett profilképe Észlelési profilkép Normál - • Az értesítések a Google Cloud Messaging rendszeren keresztül lesznek elküldve + • Az értesítések a Firebase Cloud Messaging rendszeren keresztül lesznek elküldve • Az értesítések csak metaadatokat tartalmaznak • Az értesítés tartalma közvetlenül a Matrix szerverről kerül letöltésre Az értesítések meta- és üzenet adatot is tartalmaznak @@ -1148,7 +1148,7 @@ A következő képernyőn el kell fogadnod, hogy a Riot folyamatosan fusson a h Érvényes Google Play Services APK nem található. Az értesítések megbízhatatlanul működhetnek. - Riot.im - Beszélgess, ahogy tetszik. + Riot.im - Beszélgess, ahogy tetszik Egy biztonságos és univerzális csevegő alkalmazás az irányításod alatt. A csevegő alkalmazás ami személyre szabható és az irányításod alatt marad. Riot megteremti a lehetőséget, hogy úgy beszélgess ahogy szeretnél. A [matrix] hálózathoz tervezve - ami egy nyílt és elosztott hálózat. \n @@ -1384,4 +1384,22 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Válassz Exponálás hang lejátszása + Olvasottnak jelöl + Az alkalmazásnak nem kell a háttérben folyamatosan a Matrix szerverrel tartani a kapcsolatot, ez csökkentheti az akkumulátor használatot + + %1$s: 1 üzenet + %1$s: %2$d üzenet + + + %d értesítés + %d értesítés + + + Új esemény + Szoba + Új üzenetek + Új meghívók + Én + ** A küldés nem sikerült - kérlek nyisd meg a szobát + diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index 38886943..55d85d1b 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -898,7 +898,7 @@ Attenzione: questo file può essere eliminato se l\'applicazione viene disinstal Normale Privacy ridotta L\'app richiede il permesso per funzionare in sottofondo - • Le notifiche vengono inviate via Google Cloud Messaging + • Le notifiche vengono inviate via Firebase Cloud Messaging • Le notifiche contengono solo metadati • Il contenuto del messaggio di una notifica si trova al sicuro direttamente dal server home di Matrix • Le notifiche contengono dati e metadati sul messaggio @@ -1195,22 +1195,22 @@ Nella schermata successiva ti verrà chiesto di consentire a Riot di funzionare Nessun valido APK Google Play Services è stato trovato. Le notifiche non funzioneranno correttamente. - Riot.im - Comunica, a modo tuo. + Riot.im - Comunica, a modo tuo Un\'app universale di chat sicure interamente sotto il tuo controllo. - Un\'app di chat, sotto il tuo controllo e interamente flessibile. Riot ti permette di comunicare a modo tuo. Creata per [matrix] - lo standard per le comunicazioni aperte, decentralizzate. -\n -\nOttieni un account matrix.org gratuito, ottieni il tuo server su https://modular.im, o usa un altro server Matrix. -\n -\nPerchè scegliere Riot.im\? -\n -\n• COMUNICAZIONE COMPLETA: crea stanze per i tuoi team, i tuoi amici, la tua comunità - come preferisci! Chatta, condividi file, aggiungi widget e fai videochiamate vocali - tutto gratuito. -\n -\n• GRANDI INTEGRAZIONI: usa Riot.im con gli strumenti che conosci ed ami. Con Riot.im puoi addirittura chattare con utenti e gruppi su altre app di chat. -\n -\n• PRIVATO E SICURO: tieni segrete le tue conversazioni. Una crittografia end-to-end allo stato dell\'arte assicura che le comunicazioni private restino tali. -\n -\n• OPEN, NON CHIUSO: open source e costruito su Matrix. Possiedi i tuoi dati ospitando il tuo server personale, o scegliendone uno di cui ti fidi. -\n + Un\'app di chat, sotto il tuo controllo e interamente flessibile. Riot ti permette di comunicare a modo tuo. Creata per [matrix] - lo standard per le comunicazioni aperte, decentralizzate. +\n +\nOttieni un account matrix.org gratuito, ottieni il tuo server su https://modular.im, o usa un altro server Matrix. +\n +\nPerché scegliere Riot.im\? +\n +\n• COMUNICAZIONE COMPLETA: crea stanze per i tuoi team, i tuoi amici, la tua comunità - come preferisci! Chatta, condividi file, aggiungi widget e fai videochiamate vocali - tutto gratuito. +\n +\n• GRANDI INTEGRAZIONI: usa Riot.im con gli strumenti che conosci ed ami. Con Riot.im puoi addirittura chattare con utenti e gruppi su altre app di chat. +\n +\n• PRIVATO E SICURO: tieni segrete le tue conversazioni. Una crittografia end-to-end allo stato dell\'arte assicura che le comunicazioni private restino tali. +\n +\n• OPEN, NON CHIUSO: open source e costruito su Matrix. Possiedi i tuoi dati ospitando il tuo server personale, o scegliendone uno di cui ti fidi. +\n \n• OVUNQUE TU SIA: resta in contatto ovunque tu sia con la cronologia dei messaggi totalmente sincronizzata tra i tuoi dispositivi ed online su https://riot.im. Videochiamata in corso… @@ -1421,4 +1421,30 @@ Per essere certo di non perdere nulla, mantieni gli aggiornamenti attivi." Usa configurazione + Inizializzazione del servizio + Multimedia + Compressione predefinita + Scegli + Sorgente multimediale predefinita + Scegli + Riproduci suono otturatore + + Segna come letto + L\'app non ha bisogno di connettersi in background all\'homeserver, dovrebbe ridurre il consumo della batteria + + %1$s: 1 messaggio + %1$s: %2$d messaggi + + + %d notifica + %d notifiche + + + Nuovo evento + Stanza + Nuovi messaggi + Nuovo invito + Io + ** Invio fallito - per favore apri la stanza + diff --git a/vector/src/main/res/values-nl/strings.xml b/vector/src/main/res/values-nl/strings.xml index f7957eba..f35701b4 100755 --- a/vector/src/main/res/values-nl/strings.xml +++ b/vector/src/main/res/values-nl/strings.xml @@ -3,76 +3,77 @@ nl - VS + NL Berichten - Ruimte + Kamer Instellingen - Deelnemer Details + Info over deelnemer Historisch - Akkoord + Oké Annuleren Opslaan Verlaten Versturen - Kopïeren + Kopiëren Opnieuw versturen Verwijderen - Citeer + Citeren Delen Later Doorsturen Permalink - Bekijk bron - Bekijk onversleutelde bron + Bron weergeven + Ontsleutelde bron weergeven Verwijderen - Herbenoemen + Hernoemen Inhoud melden - Actief gesprek - Lopend vergadergesprek.\nNeem deel met %1$s of %2$s. + Actieve oproep + Lopend vergadergesprek. +\nNeem deel met %1$s of %2$s microfoon camera - Kan het gesprek niet starten, probeer later nog eens - Sommige functies zijn misschien afwezig door ontbrekende rechten… - Je moet de uitnodigingspermissie hebben om een vergadering in deze ruimte te starten - Kan het gesprek niet starten - Toestel informatie - Vergadergesprekken zijn niet ondersteund in versleutelde ruimtes + Kan de oproep niet starten, probeer het later nog eens + Sommige functies zijn misschien afwezig wegens ontbrekende rechten… + Om een vergadering in deze kamer te starten heeft u uitnodigingsrechten nodig + Kan de oproep niet starten + Apparaatinformatie + Vergadergesprekken worden niet ondersteund in versleutelde kamers Toch sturen of Uitnodigen - Uitloggen + Afmelden Spraakoproep - Videogesprek + Video-oproep Globaal zoeken Alles als gelezen markeren Historisch Snel reageren - Open - Sluit - Naar klembord gekopïeerd - Deactiveren + Openen + Sluiten + Gekopieerd naar klembord + Uitschakelen Bevestiging Waarschuwing - Home + Thuis Favorieten Personen - Ruimtes + Kamers - Zoek naar ruimtes - Zoek naar favorieten - Zoek naar personen - Zoek naar ruimtes + Kamernamen filteren + Favorieten filteren + Personen filteren + Kamernamen filteren Uitnodigingen @@ -81,68 +82,72 @@ Gesprekken Lokale contactenlijst - Alleen Matrix contacten + Alleen Matrix-contacten Geen gesprekken - Je hebt Riot geen toegang tot je lokale contacten gegeven + U heeft Riot geen toegang tot uw lokale contacten gegeven Geen resultaten - Ruimtes - Ruimte adresboek - Geen ruimtes - Geen publieke ruimtes beschikbaar + Kamers + Kameradresboek + Geen kamers + Geen publieke kamers beschikbaar 1 gebruiker %d gebruikers Logboek versturen - Crash logboek versturen - Schermafbeelding versturen + Crash-logboek versturen + Schermafdruk versturen Fout melden - Beschrijf de fout. Wat deed je? Wat verwachtte je dat er zou gebeuren? Wat gebeurde er echt? - Beschrijf hier je probleem - Om het probleem te kunnen diagnosticeren, worden logboeken van deze applicatie met de foutmelding verstuurd. Deze foutmelding, inclusief de logboeken en schermafbeelding, zullen niet publiekelijk zichtbaar zijn. Als je liever alleen de bovenstaande tekst verstuurt, haal dan het vinkje weg: - Het ziet er naar uit dat je de telefoon in frustratie schudt. Wil je een probleem melden? - De foutmelding is succesvol verstuurd - Het is niet gelukt de foutmelding te versturen (%s) - Progressie (%s%%) - De applicatie was de vorige keer gecrasht. Wil je dit melden? + Beschrijf de fout. Wat heeft u gedaan\? Wat verwachtte u dat er zou gebeuren\? Wat is er echt gebeurd\? + Beschrijf hier uw probleem + Om het probleem te kunnen onderzoeken worden logboeken van deze cliënt met de foutmelding verstuurd. Deze foutmelding, inclusief de logboeken en schermafdruk, zullen niet openbaar zichtbaar zijn. Indien u liever alleen de bovenstaande tekst verstuurt, haal dan het vinkje weg: + Het ziet er naar uit dat u de telefoon in frustratie schudt. Wilt u een probleem melden\? + De foutmelding is verzonden + Verzenden van foutmelding is mislukt (%s) + Voortgang (%s%%) + De toepassing is de vorige keer gecrasht. Wilt u dit melden\? - Versturen als + Versturen naar Gelezen - Ruimte toetreden + Kamer toetreden Gebruikersnaam - Registreren - Log in - Uitloggen - Thuisserver URL - Identiteit server URL - Zoek + Account aanmaken + Aanmelden + Afmelden + Thuisserver-URL + Identiteitsserver-URL + Zoeken - Start Nieuwe Chat - Start spraakoproep - Start videogesprek + Nieuw gesprek beginnen + Spraakoproep beginnen + Video-oproep beginnen Bestanden versturen Foto of video maken - Inloggen + Aanmelden Account aanmaken Indienen Overslaan - Verstuur reset e-mail - Ga terug naar het loginscherm - E-mail of gebruikersnaam + E-mail voor opnieuw instellen versturen + Terug naar het aanmeldingsscherm + E-mailadres of gebruikersnaam Wachtwoord Nieuw wachtwoord Gebruikersnaam - "Voeg een e-mailadres aan je account toe zodat gebruikers je kunnen vinden en zodat je het wachtwoord kan veranderen." - "Voeg een telefoonnummer toe zodat gebruikers je kunnen vinden." - "Voeg een e-mailadres en/of telefoonnummer aan je account toe zodat gebruikers je kunnen vinden.\n\nHet e-mailadres maakt het ook mogelijk om je wachtwoord te veranderen." - "Voeg een e-mailadres en een telefoonnummeraan je account toe zodat gebruikers je kunnen vinden.\n\nHet e-mailadres maakt het ook mogelijk om je wachtwoord te veranderen." + Voeg een e-mailadres aan uw account toe zodat gebruikers u kunnen vinden en zodat u het wachtwoord kunt veranderen. + Voeg een telefoonnummer aan uw account toe zodat gebruikers u kunnen vinden. + Voeg een e-mailadres en/of telefoonnummer aan uw account toe zodat gebruikers u kunnen vinden. +\n +\nHet e-mailadres maakt het ook mogelijk om uw wachtwoord te veranderen. + Voeg een e-mailadres en een telefoonnummer aan uw account toe zodat gebruikers u kunnen vinden. +\n +\nHet e-mailadres maakt het ook mogelijk om uw wachtwoord te veranderen. E-mailadres E-mailadres (optioneel) Telefoonnummer @@ -152,55 +157,59 @@ Verkeerde gebruikersnaam en/of wachtwoord Gebruikersnamen mogen alleen letters, cijfers, punten en afbreek- en lage streepjes bevatten Wachtwoord te kort (min 6) - Het wachtwoord mist + Wachtwoord ontbreekt Dit is geen geldig e-mailadres "Dit is geen geldig telefoonnummer" Dit e-mailadres is al in gebruik. - Het e-mailadres mist - Het telefoonnummer mist - Het e-mailadres of telefoonnummer mist + E-mailadres ontbreekt + Telefoonnummer ontbreekt + E-mailadres of telefoonnummer ontbreekt Ongeldig bewijs De wachtwoorden komen niet overeen Wachtwoord vergeten? - Gebruik aangepaste serverinstellingen (geavanceerd) - Bekijk je e-mail om verder te gaan met het registreren - Registratie met een e-mailadres en telefoonnummer tegelijkertijd is nog niet ondersteund totdat de api bestaat. Alleen het telefoonnummer zal meegenomen worden.\n\nJe kan je e-mailadres aan je profiel toevoegen in de instellingen. - Deze thuisserver wil graag weten of je een robot bent + Aangepaste serverinstellingen gebruiken (geavanceerd) + Bekijk uw e-mail om verder te gaan met het registreren + Registratie met een e-mailadres en telefoonnummer tegelijkertijd wordt, totdat de API bestaat, nog niet ondersteund. Alleen het telefoonnummer zal meegenomen worden. +\n +\nU kunt uw e-mailadres aan uw profiel toevoegen in de instellingen. + Deze thuisserver wil graag weten of u geen robot bent Gebruikersnaam al in gebruik Thuisserver: Identiteitsserver: Ik heb mijn e-mailadres geverifieerd - Om het wachtwoord te resetten moet je het e-mailadres dat aan uw account gekoppeld is invoeren: - Het e-mailadres dat aan uw account gekoppeld is moet worden ingevoerd. - Een nieuw wachtwoord moet worden ingevoerd. - Er is een e-mail verstuurd naar %s. Klik hieronder zodra je de link in de e-mail hebt bezocht. - Het verifiëren is mislukt: wees er zeker van dat je op de link in de e-mail hebt geklikt - Je wachtwoord is gereset.\n\nJe bent op alle apparaten uitgelogd en zal niet langer push notificaties ontvangen. Om notificaties op nieuw aan te zetten, log op elk apparaat opnieuw in. + Om uw wachtwoord opnieuw in te stellen, moet u het e-mailadres dat aan uw account gekoppeld is invoeren: + Het e-mailadres dat aan uw account gekoppeld is moet ingevoerd worden. + Er moet een nieuw wachtwoord ingevoerd worden. + Er is een e-mail verstuurd naar %s. Klik hieronder zodra u de koppeling in de e-mail hebt bezocht. + Verifiëren van het e-mailadres is mislukt: zorg dat u op de koppeling in de e-mail hebt geklikt + Uw wachtwoord is opnieuw ingesteld. +\n +\nU bent op alle apparaten afgemeld en zult niet langer pushmeldingen ontvangen. Om meldingen opnieuw in te schakelen, meldt u zich op elk apparaat opnieuw aan. URL moet met http[s]:// beginnen - Inloggen mislukt: Netwerk fout - Inloggen mislukt - Registreren mislukt: Netwerk fout + Aanmelden mislukt: netwerkfout + Aanmelden mislukt + Registreren mislukt: netwerkfout Registreren mislukt - Registreren mislukt: e-mail eigenaarschap fout + Registreren mislukt: e-mail-eigendomsfout Voer een geldige URL in - Incorrecte gebruikersnaam/wachtwoord - Het toegangsbewijs dat was gespecificeerd was niet herkent + Ongeldige gebruikersnaam/wachtwoord + Het opgegeven toegangsbewijs werd niet herkend Ongeldige JSON - Bevatte geen geldeige JSON - Te veel verzoeken waren verstuurd + Bevatte geen geldige JSON + Er zijn te veel verzoeken verstuurd Deze gebruikersnaam is al in gebruik - Er is nog niet op de link in de e-mail geklikt + Er is nog niet op de koppeling in de e-mail geklikt - Je moet opnieuw inloggen om end-to-endbeveiligingssleutels te genereren voor dit apparaat en om de publieke sleutel naar de thuisserver te sturen. -Dit is eenmalig. -Excuses voor het ongemak. + U moet zich opnieuw aanmelden om de sleutels voor eind-tot-eind-versleuteling voor dit apparaat te genereren, en om de publieke sleutel naar uw thuisserver te sturen. +\nDit is eenmalig. +\nExcuses voor het ongemak. - Gelezen berichten + Leesbevestigingslijst @@ -222,23 +231,23 @@ Excuses voor het ongemak. Vandaag - Ruimtenaam - Ruimte onderwerp + Kamernaam + Kameronderwerp Oproep verbonden Oproep is aan het verbinden… - Oproep beëindigt + Oproep beëindigd Bellen… Inkomende oproep - Inkomend videogesprek + Inkomende video-oproep Inkomende spraakoproep Oproep gaande… - Het is de andere kant niet gelukt om op te nemen. - Media verbinding is mislukt + De andere kant heeft niet opgenomen. + Mediaverbinding is mislukt Kan de camera niet initialiseren - oproep ergens anders opgenomen + oproep elders opgenomen Een afbeelding of video maken" @@ -246,16 +255,26 @@ Excuses voor het ongemak. Informatie - Riot heeft toegang nodig tot je media bestanden om foto\'s te laten zien of op te slaan. - -Sta toegang toe op de volgende pop-up om bestanden vanaf je telefoon te sturen. - Riot heeft toegang nodig tot je camera om afbeeldingen en video oproepen te maken.. - \n\nGeef tijdens de volgende pop-up toegang tot de camera om een oproep te maken. - Riot heeft toegang nodig tot je microfoon om spraakoproepen te maken. - \n\nGeef tijdens de volgende pop-up toegang tot de microfoon om een oproep te maken. - Riot heeft permissie nodig om je camera en microfoon te gebruiken voor videogesprekken.\n\nGeef toegang op de volgende pop-ups om een oproep te maken. - Je adresboek e-mails worden geupload om matrix IDs te zoeken. - Je adresboek e-mails zijn geupload voor het zoeken van matix IDs.\nGeef je Riot toegang tot je contacten ? + Riot heeft toegang nodig tot uw mediabestanden om bijlagen te verzenden en op te slaan. +\n +\nVerleen toegang op de volgende pop-up om bestanden vanaf uw telefoon te sturen. + Riot heeft toegang nodig tot uw camera om foto’s en video-oproepen te maken. + " +\n +\nVerleen toegang op de volgende pop-up om de oproep te maken." + Riot heeft toegang nodig tot uw microfoon om spraakoproepen te maken. + " +\n +\nVerleen toegang op de volgende pop-up om de oproep te maken." + Riot heeft toegang nodig tot uw camera en microfoon om video-oproepen te maken. +\n +\nVerleen toegang op de volgende pop-ups om de oproep te maken. + Riot heeft toegang nodig tot uw adresboek om andere Matrix-gebruikers te vinden aan de hand van hun e-mailadressen en telefoonnummers. +\n +\nVerleen toegang op de volgende pop-up om gebruikers op Riot te ontdekken via uw adresboek. + Riot heeft toegang nodig tot uw adresboek om andere Matrix-gebruikers te vinden aan de hand van hun e-mailadressen en telefoonnummers. +\n +\nRiot toegang verlenen tot uw contacten\? Sorry. De actie is niet toegepast vanwege ontbrekende rechten @@ -264,7 +283,7 @@ Sta toegang toe op de volgende pop-up om bestanden vanaf je telefoon te sturen.< In downloads opslaan? JA NEE - Ga verder + Verdergaan Verwijderen @@ -276,126 +295,128 @@ Sta toegang toe op de volgende pop-up om bestanden vanaf je telefoon te sturen.< Spring naar het eerste ongelezen bericht. - Je bent uitgenodigd om deze ruimte toe te treden bij %s - Deze uitnodiging is naar %s verstuurd. Maar diegene is niet geassocieerd met dit account.\nMisschien wil je met een ander account inloggen of deze e-mail aan dit account toevoegen. - Je probeert aan %s deel te nemen. Zou je willen toetreden om aan de discussie deel te nemen? - een ruimte - Dit is een voorvertoning van de ruimte. Ruimte interacties zijn uitgezet. + %s heeft u uitgenodigd in deze kamer + Deze uitnodiging is naar %s verstuurd, maar die is niet geassocieerd met deze account. +\nMisschien wilt u zich met een andere account aanmelden, of dit e-mailadres aan deze account toevoegen. + U probeert toegang te verkrijgen tot %s Zou u de kamer willen toetreden om aan het gesprek deel te nemen\? + een kamer + Dit is een voorvertoning van deze kamer. Kamerinteracties zijn uitgeschakeld. - Nieuwe Chat + Nieuw gesprek Deelnemer toevoegen 1 deelnemer - Ruimte verlaten - Weet je zeker dat je de ruimte wilt verlaten? - Weet je zeker dat je %s van deze chat wilt verwijderen? - Maak + Kamer verlaten + Weet u zeker dat u de kamer wilt verlaten\? + Weet u zeker dat u %s uit dit gesprek wilt verwijderen\? + Aanmaken Online Offline Afwezig - BEHEERDER GEREEDSCHAPPEN - OPROEP - PRIVE CHATS + BEHEERDERSGEREEDSCHAPPEN + BELLEN + ÉÉN-OP-ÉÉN-GESPREKKEN APPARATEN Uitnodigen - Deze ruimte verlaten - Van deze ruimte verwijderen + Deze kamer verlaten + Verwijderen uit deze kamer Verbannen Ontbannen - Rest naar normale gebruiker - Maak moderator - Make beheerder - Maak alle berichten van deze gebruiker onzichtbaar - Maak alle berichten van deze gebruiker zichtbaar - Zoeken/uitnodigen bij naam, e-mail, id - Vermeld - Laat de lijst met apparaten zien - Je kan deze veranderingen niet ongedaan maken want je promoot de gebruiker tot dezelfde rechten als jezelf.\n Weet je het zeker? + Herinstellen als normale gebruiker + Benoemen tot moderator + Benoemen tot beheerder + Alle berichten van deze gebruiker verbergen + Alle berichten van deze gebruiker tonen + Gebruikers-ID, naam of e-mailadres + Vermelden + Lijst met apparaten weergeven + U kunt deze veranderingen niet ongedaan maken aangezien u de gebruiker tot hetzelfde niveau als uzelf promoveert. +\nWeet u het zeker\? - "Weet je zeker dat je %s in dit gesprek wil uitnodigen?" + Weet u zeker dat u %s in dit gesprek wilt uitnodigen\? - Bij ID uitnodigen + Uitnodigen met ID LOKALE CONTACTEN (%d) - Alleen Matrix gebruikers + Enkel Matrix-gebruikers - Gebruiker bij ID uitnodigen - Vul één of meer e-mailaddressen of Matrix ID\'s in - E-mail of Matrix ID + Gebruiker uitnodigen met ID + Voer één of meer e-mailadressen of Matrix-ID’s in + E-mailadres of Matrix-ID Zoeken %s is aan het typen… - %1$s & %2$s zijn aan het typen… - %1$s & %2$s & anderen zijn aan het typen… + %1$s en %2$s zijn aan het typen… + %1$s, %2$s en anderen zijn aan het typen… De verbinding met de server is verbroken. Verstuur een versleuteld bericht… Verstuur een bericht (niet versleuteld)… - Berichten niet verstuurd. Nu %1$s of %2$s? + Berichten zijn niet verstuurd. Nu %1$s of %2$s\? Berichten zijn niet verstuurd omdat er onbekende apparaten aanwezig zijn. Nu %1$s of %2$s? - Alles opnieuw versturen - Alles annuleren - Verstuur niet verstuurde berichten - Verwijder niet gestuurde berichten + alles opnieuw versturen + alles annuleren + Onverstuurde berichten opnieuw versturen + Onverstuurde berichten verwijderen Bestand niet gevonden - Je hebt niet de permissie om dit naar deze ruimte te sturen + U heeft geen toestemming om dit naar deze kamer te sturen Vertrouwen Niet vertrouwen - Uitloggen + Afmelden Negeren Vingerafdruk (%s): - Kan de identiteit van de server niet verifïeren. - Dit kan betekenen dat iemand je internetverkeer met boosaardige intenties probeert te onderscheppen, of dat je telefoon het certificaat van de server niet vertrouwd. - Als de serverbeheerder heeft gezegd dat dit normaal is, wees er dan zeker van dat het vingerafdruk hieronder overeenkomt met de door de beheerder verschafde vingerafdruk. - Het certificaat is veranderd van één dat was vertrouwd bij je telefoon tot een ander Dit is HEEL ONGEBRUIKELIJK. Het is aangeraden om dit nieuwe certificaat NIET TE ACCEPTEREN. - Het certificaat is veranderd van een vertrouwd certificaat naar een certificaat dat niet vertrouwd is. De server heeft misschien zijn certificaat vernieuwd. Contacteer de serverbeheerder voor het verwachte vingerafdruk. - Accepteer het certificaat alleen als de serverbeheerder een vingerafdruk heeft gepubliceerd die overeenkomt met degene hierboven. + Kan de identiteit van de externe server niet verifïeren. + Dit kan betekenen dat iemand uw internetverkeer met slechte bedoelingen probeert te onderscheppen, of dat uw telefoon het certificaat van de server niet vertrouwt. + Als de serverbeheerder heeft gezegd dat dit normaal is, wees er dan zeker van dat de vingerafdruk hieronder overeenkomt met de door de beheerder verschafte vingerafdruk. + Het certificaat is veranderd van één dat door uw telefoon werd vertrouwd naar een ander. Dit is HEEL ONGEBRUIKELIJK. Het wordt aangeraden om dit nieuwe certificaat NIET TE AANVAARDEN. + Het certificaat is veranderd van een vertrouwd naar een onvertrouwd certificaat. De server heeft misschien zijn certificaat vernieuwd. Contacteer de serverbeheerder voor de verwachte vingerafdruk. + Aanvaard het certificaat alleen als de serverbeheerder een vingerafdruk heeft gepubliceerd die overeenkomt met degene hierboven. - Ruimte Details + Info over kamer Personen Bestanden Instellingen - Incorrecte ID. Het moet een e-mailadres of een Matrix ID moeten zijn dat lijkt op \'@localpart:domein\' + Ongeldige ID. Het zou een e-mailadres of een Matrix-ID zoals ‘@gebruikersnaam:domein’ moeten zijn UITGENODIGD TOEGETREDEN - Reden voor het aangeven van deze content - Wil je alle berichten van deze gebruiker onzichtbaar maken? - -Let op: deze actie zal de app opnieuw opstarten, en kan enige tijd kosten. + Reden voor het melden van deze inhoud + Wilt u alle berichten van deze gebruiker verbergen\? +\n +\nLet op: deze actie zal de app opnieuw opstarten; dit kan even duren. Upload annuleren Download annuleren Zoeken - Zoek bij naam of id + Deelnemers van kamer filteren Geen resultaten - RUIMTES + KAMERS BERICHTEN PERSONEN BESTANDEN - TREED TOE + TOETREDEN ADRESBOEK FAVORIETEN - RUIMTES + KAMERS LAGE PRIORITEIT UITNODIGINGEN - Start chat - Ruimte maken - Join room - Join a room - Type a room id or a room alias + Gesprek beginnen + Kamer aanmaken + Kamer toetreden + Treed een kamer toe + Voer een kamer-ID of -alias in Adresboek doorbladeren @@ -403,17 +424,17 @@ Let op: deze actie zal de app opnieuw opstarten, en kan enige tijd kosten. Favoriet - De-prioriteren - Privé chat - Gesprek Verlaten - Forget + Lage prioriteit + Eén-op-één-gesprek + Gesprek verlaten + Vergeten Berichten Instellingen Versie Algemene voorwaarden - Derde partij vermeldingen + Derdepartijvermeldingen Copyright Privacybeleid @@ -424,88 +445,89 @@ Let op: deze actie zal de app opnieuw opstarten, en kan enige tijd kosten.E-mailadres E-mailadres toevoegen Telefoonnummer - Add phone number - Applicatiesysteem informatiescherm - Applicatie informatie + Telefoonnummer toevoegen + Toon informatie over de app in de systeeminstellingen. + App-informatie - Notificaties voor dit account aanzetten - Notificaties voor dit apparaat aanzetten - Het scherm voor drie seconden aanzetten + Meldingen voor deze account inschakelen + Meldingen voor dit apparaat inschakelen + Het scherm voor 3 seconden aanzetten - Berichten in privé-chats - Berichten in groepchats - Wanneer ik voor een groep uitgenodigd word - Gesprek uitnodigingen - Door de robot verstuurde berichten + Berichten in één-op-één-gesprekken + Berichten in groepsgesprekken + Wanneer ik in een kamer word uitgenodigd + Oproepuitnodigingen + Door een robot verstuurde berichten Synchronisatie in de achtergrond - Achtergrond sync aanzetten - Sync verzoek is verlopen + Achtergrondssynchronisatie inschakelen + Synchronisatieverzoek is verlopen Pauze tussen elk verzoek seconde seconden Versie - olm versie + olm-versie Algemene voorwaarden - Derde partij vermeldingen + Derdepartijvermeldingen Copyright Privacybeleid - Cache verwijderen + Cache wissen Gebruikersinstellingen - Notificaties + Meldingen Genegeerde gebruikers - Anders + Overige Geavanceerd Cryptografie - Notificatie doelen + Meldingsdoelen Lokale contacten - Contacten permissie - Telefoonboek land - Home scherm - Pin ruimtes met gemiste notificaties - Pin ruimtes met ongelezen berichten + Contacten-toestemming + Land voor telefoonboek + Startscherm + Kamers met gemiste meldingen vastprikken + Kamers met ongelezen berichten vastprikken Apparaten - Apparaatdetails + Apparaatinformatie ID Naam Apparaatnaam Laatst gezien %1$s @ %2$s - Deze actie vereist additionele authenticatie.\nVoer je wachtwoord in om verder te gaan. + Deze actie vereist bijkomende authenticatie. +\nVoer uw wachtwoord in om verder te gaan. Authenticatie Wachtwoord: - Sturen + Indienen - Ingelogd als + Aangemeld als Thuisserver Identiteitsserver - Verificatie afwachtend - Bekijk je e-mail en klik op de link dat de e-mail bevat. Zodra dit gedaan is, klik op verder gaan. - Het verifïeren van het e-mailadres is mislukt. Bekijk je e-mail en klik op de link dat de e-mail bevat. Zodra dit gedaan is, klik op verder gaan + Verificatie in afwachting + Bekijk uw e-mail en tik op de koppeling erin. Tik zodra dit gedaan is op Verdergaan. + Het verifiëren van het e-mailadres is mislukt. Bekijk uw e-mail en tik op de koppeling erin. Tik zodra dit gedaan is op Verdergaan. - This email address is already in use - Failed to send email: This email address was not found - This phone number is already in use + Dit e-mailadres is al in gebruik. + Dit e-mailadres is niet gevonden. + Dit telefoonnummer is al in gebruik. Wachtwoord veranderen - oud wachtwoord - nieuw wachtwoord - wachtwoord bevestigen - Wachtwoord wijziging is mislukt - Je wachtwoord is gewijzigd - Alle berichten van %s laten zien? + Huidig wachtwoord + Nieuw wachtwoord + Nieuw wachtwoord bevestigen + Bijwerken van wachtwoord is mislukt + Uw wachtwoord is gewijzigd + Alle berichten van %s tonen\? +\n +\nLet op: deze actie zal de app herstarten; dit kan even duren. -Let op: deze actie zal de app opnieuw opstarten, en kan enige tijd kosten. + Weet u zeker dat u dit meldingsdoel wilt verwijderen\? - Weet je zeker dat je dit notificatiedoel wilt verwijderen? - - Weet je zeker dat je de %1$s %2$s wilt verwijderen? + Weet u zeker dat u de %1$s %2$s wilt verwijderen\? Kies een land @@ -513,19 +535,19 @@ Let op: deze actie zal de app opnieuw opstarten, en kan enige tijd kosten.Kies een land Telefoonnummer Ongeldig telefoonnummer voor het geselecteerde land - Telefoon verificatie - "We hebben je een SMS met een activeringscode gestuurd. Vul deze code hieronder in." + Telefoonverificatie + We hebben u een sms met een activeringscode gestuurd. Voer deze code hieronder in. Voer een activeringscode in - Er is een fout opgetreden tijdens het valideren van het telefoonnummer + Er is een fout opgetreden bij het valideren van uw telefoonnummer Code - Ruimte afbeelding - Ruimtenaam + Kamerafbeelding + Kamernaam Onderwerp - Ruimte label + Kamerlabel Gelabeld als: @@ -535,149 +557,153 @@ Let op: deze actie zal de app opnieuw opstarten, en kan enige tijd kosten. Toegankelijk- en zichtbaarheid - Vermeld deze ruimte in het adresboek - Ruimte toegankelijkheid - Leesbaarheid van de geschiedenis - Wie kan de geschiedenis lezen? - Wie heeft toegang tot deze ruimte? + Deze kamer vermelden in het adresboek + Toegang tot kamer + Toegang tot de kamergeschiedenis + Wie kan er de geschiedenis lezen\? + Wie heeft er toegang tot deze kamer\? Iedereen - Alleen deelnemers (vanaf de tijd dat deze optie is geselecteerd) - Alleen deelnemers (vanaf de tijd dat ze worden uitgenodigd) - Alleen deelnemers (vanaf de tijd dat ze toetreden) + Alleen deelnemers (vanaf het moment dat deze optie wordt geselecteerd) + Alleen deelnemers (vanaf het moment dat ze worden uitgenodigd) + Alleen deelnemers (vanaf het moment dat ze toetreden) - Om naar een ruimte te verwijzen moeten het een adres hebben. + Om naar een kamer te verwijzen moet deze een adres hebben. Alleen personen die uitgenodigd zijn - Iedereen die de link van de ruimte weet, met uitzondering van gasten - Iedereen die de link van de ruimte weet, inclusief gasten + Iedereen die de koppeling van de kamer kent, met uitzondering van gasten + Iedereen die de koppeling van de kamer kent, inclusief gasten Verbannen gebruikers Geavanceerd - Interne ID van deze ruimte + Interne ID van deze kamer Adressen - Labs - Dit zijn experimentele functies die op onverwachte manieren kunnen breken. Wees behoedzaam bij het gebruik van deze functies. - End-to-endbeveiliging - Je moet uitloggen om de versleuteling aan te zetten. - End-to-endbeveiliging is actief - Alleen naar geverifieerde apparaten versturen - Nooit in deze ruimte niet geverifieerde apparaten berichten sturen vanaf dit apparaat. + Experimenteel + Dit zijn experimentele functies die zich op onverwachte manieren kunnen gedragen. Wees behoedzaam bij het gebruik van deze functies. + Eind-tot-eind-versleuteling + Om de versleuteling in te schakelen dient u zich eerst af te melden. + Eind-tot-eind-versleuteling is actief + Alleen naar geverifieerde apparaten versleutelen + Ongeverifieerde apparaten in deze kamer nooit berichten sturen vanaf dit apparaat. - Deze ruimte heeft geen lokale adressen - Nieuw addres (bijv. #foo:matrix.org") + Deze kamer heeft geen lokale adressen + Nieuw adres (bv. #foo:matrix.org) - Ongeldig alias formaat - \'%s\' is niet een geldige alias formaat - Je zal geen standaard adres gespecificeerd hebben. Het standaard adres voor deze ruimte zal willekeurig gekozen worden" - Standaard adres waarschuwingen + Ongeldig aliasformaat + ‘%s’ is geen geldig aliasformaat + U zult geen hoofdadres voor deze kamer opgegeven hebben. + Hoofdadreswaarschuwingen - Als standaard adres instellen - Niet als standaard adres instellen - Ruimte ID kopïeren - Ruimte adres kopïeren + Instellen als hoofdadres + Niet instellen als hoofdadres + Kamer-ID kopiëren + Kameradres kopiëren - Versleuteling is aan in deze ruimte. - Versleuteling is uit in deze ruimte. - Zet versleuteling aan \n(waarschuwing: dit kan niet meer uitgezet worden!) + Versleuteling is ingeschakeld in deze kamer. + Versleuteling is uitgeschakeld in deze kamer. + Versleuteling inschakelen +\n(let op: dit kan niet meer uitgeschakeld worden!) Adresboek - %s probeerde een specifiek punt in de geschiedenis van deze ruimte te laden, maar kon het niet vinden. + %s heeft geprobeerd een specifiek punt in de geschiedenis van deze kamer te laden, maar kon het niet vinden. - Informatie over end-to-endbeveilig + Informatie over eind-tot-eind-versleuteling Evenementsinformatie Gebruikers-ID Curve25519-identiteitssleutel - Geclaimde Ed25519-sleutelvingerafdruk + Geclaimde Ed25519-vingerafdrukssleutel Algoritme - Sessie ID + Sessie-ID Ontsleutelingsfout - Afzenderapparaatinformatie + Informatie over apparaat van afzender Apparaatnaam Naam - Apparaat-ID - Apparaatsleutel + Apparaats-ID + Apparaatssleutel Verificatie - Ed25519 vingerafdruk + Ed25519-vingerafdruk - Exporteer E2E ruimte sleutels - Exporteer ruimte sleutels + E2E-kamersleutels exporteren + Kamersleutels exporteren Exporteer de sleutels naar een lokaal bestand - Exporteer - Enter wachtzin - Bevestig wachtzin - De E2E ruimte sleutels zijn in \'%s\' opgeslagen. + Exporteren + Voer wachtwoord in + Wachtwoord bevestigen + De E2E-kamersleutels zijn in ‘%s’ opgeslagen. +\n +\nLet op: dit bestand kan verwijderd worden als de app verwijderd is. -Waarschuwing: dit bestand kan worden verwijderd als de applicatie verwijderd is. - - Importeer E2E ruimte sleutels - Importeer ruimte sleutels + E2E-kamersleutels importeren + Kamersleutels importeren Importeer de sleutels uit een lokaal bestand - Importeer - Versleutel alleen naar geverifieerde apparaten - Verstuur nooit versleutelde berichten naar niet geverifieerde apparaten vanaf dit apparaat + Importeren + Enkel naar geverifieerde apparaten versleutelen + Versleutelde berichten nooit naar ongeverifieerde apparaten sturen vanaf dit apparaat. - NIET Geverifieerd + NIET geverifieerd Geverifieerd Geblokkeerd onbekend apparaat geen - Verifïeren - Ontverifïeren + Verifiëren + Ontverifiëren Blokkeringslijst Deblokkeringslijst - Verifieer apparaat - Om te verifiëren dat dit apparaat vertrouwd kan worden, contacteer de eigenaar via een andere methode (bijv. persoonlijk of via een telefoontje) en vraag diegene of de sleutel die ze zien in hun Gebruikersinstellingen van dit apparaat overeenkomt met de sleutel hieronder: - Als het overeenkomt, druk op de knop \'verifiëren\' hieronder. Als het niet overeenkomt, dan onderschept iemand anders het apparaat en hoor je deze te blokkeren. -In de toekomst zal dit verificatieproces wat makkelijker gemaakt worden. - Ik verifïeer dat de sleutels overeenkomen + Apparaat verifiëren + Om te verifiëren dat dit apparaat vertrouwd kan worden, contacteert u de eigenaar via een andere methode (bv. persoonlijk of via een telefoontje) en vraagt u hem/haar of de sleutel die hij/zij ziet in zijn/haar Gebruikersinstellingen van dit apparaat overeenkomt met de sleutel hieronder: + Als het overeenkomt, drukt u op de knop ‘Verifiëren’ hieronder. Als het niet overeenkomt, dan onderschept iemand anders het apparaat en zou u het beter blokkeren. In de toekomst zal dit verificatieproces verbeterd worden. + Ik verifieer dat de sleutels overeenkomen - Riot ondersteunt nu end-to-endbeveiliging maar je moet opnieuw inloggen om het aan te zetten. - -Je kan het nu, of later, doen vanuit de programma-instellingen. + Riot ondersteunt nu eind-tot-eind-versleuteling, maar u moet zich opnieuw aanmelden om het in te schakelen. +\n +\nU kunt dit nu of later doen vanuit de app-instellingen. - Deze ruimte bevat onbekende apparaten - Deze ruimte bevat onbekende apparaten die niet geen geverifieerd.\nDit betekent dat er geen garantie is dat de apparaten bij de gebruikers horen waar het beweert dat het bij hoort.\nWe raden je aan om bij elk apparaat door het verificatieprocces heen te gaan voordat je doorgaat, maar je kan het bericht opnieuw versturen zonder te verifiëren als je dat prefereert.\n\nOnbekende apparaten: + Deze kamer bevat onbekende apparaten + Deze kamer bevat onbekende apparaten die niet geverifieerd zijn. +\nDit betekent dat er geen garantie is dat de apparaten bij de gebruikers horen waartoe ze beweren te horen. +\nWe raden u aan om bij elk apparaat door het verificatieprocces heen te gaan voordat u verdergaat, maar u kunt het bericht ook zonder te verifiëren opnieuw versturen. +\n +\nOnbekende apparaten: - Selecteer een ruimte adresboek + Kies een kameradresboek Het kan zijn dat de server niet beschikbaar of overbelast is - Typ een thuisserver om de publieke ruimte ervan te weergeven - Thuisserver URL - Alle ruimtes op %s server - Alle lokale %s ruimtes + Voer een thuisserver in om de publieke kamers ervan weer te geven + Thuisserver-URL + Alle kamers op server %s + Alle lokale kamers op %s - Zoek voor historisch + Zoeken naar historische Offline Gebruikersadresboek GEBRUIKERSADRESBOEK (%s) - Start bij het inschakelen van je telefoon - Media cache vrijmaken - Media behouden + Starten bij opstarten + Mediacache wissen + Media bewaren - Tijd weergeven voor alle berichten - Data bespaarmodus + Tijdsaanduidingen weergeven voor alle berichten + Databesparingsmodus Gebruikersinterface - Interface Taal - Kies een taal + Taal + Taal kiezen 1 week 1 maand @@ -691,75 +717,75 @@ Je kan het nu, of later, doen vanuit de programma-instellingen. Groot Grootst 3 dagen - Klein + Piepklein Groter Gigantisch - Licht Thema - Donker Thema - Zwart Thema + Licht thema + Donker thema + Zwart thema - Aan het synchroniseren + Bezig met synchroniseren Luisteren voor evenementen Mobiel - Notificatie geluid + Meldingsgeluid Tijdsaanduidingen in 12-uursformaat weergeven - Je hebt permissie nodig om widgets in deze ruimte te beheren - Het maken van de widget is niet goed gegaan + U heeft toestemming nodig om widgets in deze kamer te beheren + Aanmaken van widget is mislukt %1$s is toegevoegd door %2$s %1$s is verwijderd door %2$s - Maak vergadergesprekken met jitsi - Weet je zeker dat je deze widget wilt verwijderen? + Vergadergesprekken maken met jitsi + Weet u zeker dat u deze widget uit deze kamer wilt verwijderen\? - Niet mogelijk om de widget te maken. - Niet gelukt om een verzoek te verzenden. - Het machtsniveau moet een positief en heel getal zijn. - Je zit niet in deze ruimte. - Je hebt niet de permissie om dat in deze ruimte te doen. - room_id mist in het verzoek. - user_id mist in het verzoek. - Ruimte %s is niet zichtbaar. - Matrix Apps toevoegen - Geluidgevende notificaties - Stille notificaties + Kan widget niet aanmaken. + Versturen van verzoek mislukt. + Het machtsniveau moet een positief geheel getal zijn. + U zit niet in deze kamer. + U heeft geen toestemming om dat in deze kamer te doen. + room_id ontbreekt in het verzoek. + user_id ontbreekt in het verzoek. + Kamer %s is niet zichtbaar. + Matrix-apps toevoegen + Geluidsmeldingen + Stille meldingen Foutmelding Foto maken Video maken - Bel - Berichten die mijn weergavenaam bevat - Berichten dat mijn gebruikersnaam bevat - Analytics + Bellen + Berichten die mijn weergavenaam bevatten + Berichten die mijn gebruikersnaam bevatten + Statistische gegevens - Gebruik de systeem camera + Systeemcamera gebruiken - Je hebt een nieuw apparaat toegevoegd \'%s\', dat versleutelingssleutels aanvraagt. - Je niet geverifieerde apparaat \'%s\' vraagt versleutelingssleutels aan. + U heeft een nieuw apparaat ‘%s’ toegevoegd, dat versleutelingssleutels aanvraagt. + Uw ongeverifieerde apparaat ‘%s’ vraagt versleutelingssleutels aan. Verificatie starten Delen zonder te verifiëren Verzoek negeren - Waarschuwing! - Vergadergesprekken zijn in ontwikkeling en kunnen dus kuren vertonen. + Let op! + Vergadergesprekken zijn in ontwikkeling en kunnen dus nog kuren vertonen. - Besturingssignaal fout - Onbekend besturingssignaal: %s + Opdrachtfout + Onbekende opdracht: %s Uit - Luidruchtig + Lawaaierig Versleuteld bericht - Gemeenschap details + Info over gemeenschap Laden… @@ -767,17 +793,17 @@ Je kan het nu, of later, doen vanuit de programma-instellingen. Acties Gemeenschappen - Zoeken voor gemeenschappen + Gemeenschapsnamen filteren Uitnodigen Gemeenschappen Geen groepen - Schudden om een fout te rapporteren + Schudden om een probleem te melden - Weet je zeker dat je een nieuwe chat met %s wilt starten? - Weet je zeker dat je een spraakoproep wilt starten? - Weet je zeker dat je een videogesprek wilt starten? + Weet u zeker dat u een nieuw gesprek met %s wilt beginnen\? + Weet u zeker dat u een spraakoproep wilt beginnen\? + Weet u zeker dat u een video-oproep wilt beginnen\? Groepenlijst @@ -790,14 +816,14 @@ Je kan het nu, of later, doen vanuit de programma-instellingen. Opschrift openen Synchroniseren… - 1 actief lid - %d actieve leden + 1 actieve deelnemer + %d actieve deelnemers - 1 lid - %d leden + 1 deelnemer + %d deelnemers - Weet je zeker dat je deze gebruiker uit dit gesprek wilt verbannen? + Weet u zeker dat u deze gebruiker uit dit gesprek wilt verbannen\? 1 nieuw bericht @@ -805,38 +831,38 @@ Je kan het nu, of later, doen vanuit de programma-instellingen. - 1 ruimte - %d ruimtes + 1 kamer + %d kamers - %1$s ruimte gevonden voor %2$s - %1$s ruimtes gevonden voor %2$s + %1$s kamer gevonden voor %2$s + %1$s kamers gevonden voor %2$s Alle berichten (luid) Alle berichten Alleen vermeldingen Dempen - Thuisscherm snelkoppeling toevoegen + Snelkoppeling aan thuisscherm toevoegen Inline URL-voorvertoning - Trillen bij vermeldingen + Trillen bij vermelden van een gebruiker Badge - Notificaties - Deze ruimte geeft geen badges voor gemeenschappen weer - Nieuw gemeenschaps-ID (bv. +foo:matrix.org) - Ongeldig gemeenschaps-ID - \'%s\' is niet een geldig gemeenschaps-ID + Meldingen + Deze kamer geeft geen badges voor gemeenschappen weer + Nieuwe gemeenschaps-ID (bv. +foo:matrix.org) + Ongeldige gemeenschaps-ID + ‘%s’ is geen geldige gemeenschaps-ID - 1 ongelezen bericht waarin je vermeld staat - %d ongelezen berichten waarin je vermeld staat + 1 ongelezen bericht waarin u vermeld staat + %d ongelezen berichten waarin u vermeld staat - 1 ruimte - %d ruimtes + 1 kamer + %d kamers %1$s in %2$s @@ -846,96 +872,96 @@ Je kan het nu, of later, doen vanuit de programma-instellingen. - Creëer - Gemeenschap Aanmaken + Aanmaken + Gemeenschap aanmaken Gemeenschapsnaam Voorbeeld Gemeenschaps-ID voorbeeld - Home + Thuis Personen - Ruimtes + Kamers Geen gebruikers - Ruimtes + Kamers Toegetreden Uitgenodigd Groepsleden filteren - Groepsruimtes filteren + Groepskamers filteren - De gemeenschapsadministrator heeft geen lange beschrijving gegeven voor deze gemeenschap. + De gemeenschapsbeheerder heeft geen lange beschrijving gegeven voor deze gemeenschap. - Je bent uit %1$s gezet door %2$s - Je bent verbannen van %1$s door %2$s + %2$s heeft u uit %1$s gezet + %2$s heeft u uit %1$s verbannen Reden: %1$s Opnieuw toetreden - Ruimte vergeten + Kamer vergeten Ontvangst-avatar Avatar - 1 ongelezen bericht waarin je vermeld staat - %d ongelezen berichten waarin je vermeld staat + 1 ongelezen bericht waarin u vermeld staat + %d ongelezen berichten waarin u vermeld staat Vermeldingsavatar Verstuur een sticker Sticker versturen - Je hebt momenteel geen stickerpakketten aan staan. + U heeft momenteel geen stickerpakketten ingeschakeld. +\n +\nWilt u er nu een paar toevoegen\? -Wil je er nu een paar toevoegen? - - Notificatie privacy + Meldingsprivacy Normaal Gereduceerde privacy - De applicatie heeft toestemming nodig om in de achtergrond te werken - • Notifications worden verstuurd via Google Cloud Messaging - • Notificaties bevatten alleen metadata - • De inhoud van de berichten in de notificatie is veilig overgebracht rechtstreeks vanaf de Matrix thuisserver - • Notificaties bevatten meta- en berichtdata - • Notificaties zullen niet de inhoud van het bericht weergeven + De app heeft toestemming nodig om in de achtergrond te werken + • Meldingen worden verstuurd via Firebase Cloud Messaging + • Meldingen bevatten alleen metadata + • De inhoud van de berichten in de melding is veilig overgebracht, rechtstreeks vanaf de Matrix-thuisserver + • Meldingen bevatten meta- en berichtdata + • Meldingen zullen berichtinhoud niet weergeven Account deactiveren Mijn account deactiveren - Notificatie Privacy - Riot kan in de achtergrond werken om je notificaties veilig en privé te beheren (dit kan misschien batterijgebruik aantasten). - Toestemming geven + Meldingsprivacy + Riot kan in de achtergrond werken om uw meldingen veilig en privé te beheren. Dit beïnvloedt mogelijk het accuverbruik. + Toestemming verlenen Kies een andere optie Statistische gegevens (analytics) versturen - Riot verzamelt anonieme gegevens (analytics) om het voor ons mogelijk te maken om de applicatie te verbeteren. - Zet analytics alsjeblieft aan om ons te helpen in het verbeteren van Riot. - Ja ik wil helpen! + Riot verzamelt anonieme statistische gegevens (analytics) om het voor ons mogelijk te maken om de app te verbeteren. + Schakel statistische gegevens in om ons te helpen bij het verbeteren van Riot. + Ja, ik wil helpen! - Een vereiste parameter mist. - Een parameter is niet geldig. - Om deze thuisserver verder te blijven gebruiken %1$s moet je de voorwaarden lezen en ermee akkoord gaan. + Er ontbreekt een vereiste parameter. + Er is een parameter ongeldig. + Om de %1$s-thuisserver verder te blijven gebruiken, dient u de voorwaarden te lezen en ermee akkoord te gaan. Nu doorlezen - Account Deactiveren - Dit zal je account voorgoed onbruikbaar maken. Je zal niet meer in kunnen loggen en niemand anders zal met dezelfde gebruikers ID kunnen registreren. Dit zal er voor zorgen dat je account alle ruimtes verlaat waar het momenteel onderdeel van is en het verwijderd de accountgegevens van de identiteitsserver. Deze actie is onomkeerbaar. - -Het deactiveren van je account zal er niet standaard voor zorgen dat de berichten die je verzonden hebt vergeten worden. Als je wilt dat wij de berichten vergeten, klikt alsjeblieft op het vakje hieronder. - -De zichtbaarheid van berichten in Matrix is hetzelfde als in e-mail. Het vergeten van je berichten betekent dat berichten die je hebt verstuurd niet meer gedeeld worden met nieuwe of ongeregistreerde gebruikers, maar geregistreerde gebruikers die al toegang hebben tot deze berichten zullen alsnog toegang hebben tot hun eigen kopie van het bericht. - Vergeet alle berichten die ik heb verstuurd wanneer mijn account gedeactiveerd is (Waarschuwing: dit zal er voor zorgen dat toekomstige gebruikers een incompleet beeld krijgen van gesprekken) - Om verder te gaan, voer je wachtwoord in: - Account Deactiveren + Account deactiveren + Dit zal uw account voorgoed onbruikbaar maken. U zult zich niet meer kunnen aanmelden, en niemand anders zal met dezelfde gebruikers-ID kunnen registreren. Dit zal er voor zorgen dat uw account alle kamers verlaat waar deze momenteel lid van is, en het verwijdert de accountgegevens van de identiteitsserver. Deze actie is onomkeerbaar. +\n +\nHet deactiveren van uw account zal er niet standaard voor zorgen dat de berichten die u hebt verzonden worden vergeten. Indien u wilt dat wij de berichten vergeten, vinkt u het vakje hieronder aan. +\n +\nDe zichtbaarheid van berichten in Matrix is gelijkaardig aan e-mails. Het vergeten van uw berichten betekent dat berichten die u verstuurd heeft niet meer gedeeld worden met nieuwe of ongeregistreerde gebruikers, maar geregistreerde gebruikers die al toegang hebben tot deze berichten zullen alsnog toegang hebben tot hun eigen kopie ervan. + Vergeet alle berichten die ik heb verstuurd wanneer mijn account gedeactiveerd is (Let op: dit zal er voor zorgen dat toekomstige gebruikers een onvolledig beeld krijgen van gesprekken) + Voer uw wachtwoord in om verder te gaan: + Account deactiveren Licenties van derde partijen - Download - Spreek - Beveiligingssleutels van je andere apparaten opnieuw aanvragen. + Downloaden + Inspreken + Beveiligingssleutels van uw apparaten opnieuw aanvragen. - Sleutel aanvraag verstuurd. + Sleutelaanvraag verstuurd. Aanvraag verstuurd - Start Riot op een ander apparaat dat het bericht kan ontsleutelen, zodat die de sleutels naar dit apparaat kan sturen. + Start Riot op een ander apparaat dat het bericht kan ontsleutelen, zodat het de sleutels naar dit apparaat kan sturen. Typ hier… @@ -943,41 +969,41 @@ De zichtbaarheid van berichten in Matrix is hetzelfde als in e-mail. Het vergete Spraakbericht versturen doorgaan met… - Sorry, er is geen externe applicatie gevonden om deze actie te voltooien. + Sorry, er is geen externe toepassing gevonden om deze actie te voltooien. - Verstuur stembericht (heeft een externe applicatie nodig om stemberichten op te nemen) + Stemberichten versturen - Vul alstublieft uw wachtwoord in. + Voer uw wachtwoord in. - Schrijf de omschrijving in het Engels, indien mogelijk. - Verstuur een versleutelde reactie… - Stuur een reactie (onversleuteld)… - Bekijk media voor het versturen + Beschrijf het probleem in het Engels, indien mogelijk. + Verstuur een versleuteld antwoord… + Verstuur een antwoord (onversleuteld)… + Media bekijken vóór het versturen - Je bent momenteel geen lid van een community. + U bent momenteel geen lid van een gemeenschap. - Gebruik de enterknop van het toetsenbord om een bericht te versturen - Vertoon actie - Alle berichten van deze gebruiker tonen? - -Let op: de app zal voor deze actie opnieuw opgestart worden, wat enige tijd kan kosten. - Gebruiker met gegeven ID verbannen - Heft de verbanning van de gebruiker met gegeven ID op - Machtsniveau van gebruiker instellen - Rechten van gebruiker met gegeven ID afpakken - Gebruiker met gegeven ID in de huidige ruimte uitnodigen - Ruimte met gegeven alias toetreden - Ruimte verlaten - Onderwerp van de ruimte instellen - Gebruiker met gegeven ID eruit sturen - Je weergavenaam aanpassen + Enter-knop van toetsenbord gebruiken om berichten te versturen + Toont een actie + Alle berichten van deze gebruiker tonen\? +\n +\nLet op: de app zal voor deze actie opnieuw opgestart worden; dit kan even duren. + Verbant gebruiker met gegeven ID + Heft verbanning van gebruiker met gegeven ID op + Stel het machtsniveau van een gebruiker in + Neemt rechten van gebruiker met gegeven ID af + Nodigt gebruiker met gegeven ID uit in de huidige kamer + Treedt toe tot kamer met gegeven alias + Kamer verlaten + Onderwerp van de kamer instellen + Stuurt gebruiker met gegeven ID eruit + Wijzigt uw weergavenaam Markdown aan/uit - Deze ruimte is vervangen en is niet langer actief + Deze kamer is vervangen en is niet langer actief Het gesprek wordt hier voortgezet - Deze ruimte is een voortzetting van een ander gesprek + Deze kamer is een voortzetting van een ander gesprek Klik hier om oudere berichten te zien - Door missende permissies is deze actie niet mogelijk. + Deze actie is niet mogelijk wegens ontbrekende rechten. Systeemmeldingen @@ -989,8 +1015,8 @@ Let op: de app zal voor deze actie opnieuw opgestart worden, wat enige tijd kan %dm - 1h - %dh + 1u + %du 1d @@ -998,7 +1024,7 @@ Let op: de app zal voor deze actie opnieuw opgestart worden, wat enige tijd kan nu %1$s - %1$s %2$s geleden + %2$s geleden %1$s "%1$s, " %1$s en %2$s @@ -1008,7 +1034,7 @@ Let op: de app zal voor deze actie opnieuw opgestart worden, wat enige tijd kan 1 geselecteerd %d geselecteerd - Om Matrix Apps management te repareren + Om Matrix-appbeheer te herstellen 1 deelnemer @@ -1016,54 +1042,416 @@ Let op: de app zal voor deze actie opnieuw opgestart worden, wat enige tijd kan - 1 ruimte - %d ruimtes + 1 kamer + %d kamers - Resource limiet overschreden - Contact Beheerder + Bronlimiet overschreden + Beheerder contacteren - neem contact op met je service beheerder + contact op te nemen met uw dienstbeheerder - Deze homeserver heeft een van zijn resource limieten bereikt dus sommige gebruikers zullen niet kunnen inloggen. - Deze homeserver heeft een van zijn resource limieten bereikt. + Deze thuisserver heeft een van zijn bronlimieten overschreden, dus sommige gebruikers zullen zich niet kunnen aanmelden. + Deze thuisserver heeft een van zijn bronlimieten overschreden. - Deze homeserver heeft zijn Maandelijks Actieve Gebruikers limiet bereikt dus sommige gebruikers zullen niet kunnen inloggen. - Deze homeserver heeft zijn Maandelijks Actieve Gebruikers limiet bereikt. + Deze thuisserver heeft zijn limiet voor maandelijks actieve gebruikers overschreden, dus sommige gebruikers zullen zich niet kunnen aanmelden. + Deze thuisserver heeft zijn limiet voor maandelijks actieve gebruikers overschreden. - Alstublieft %s om dit limiet te verhogen. - Alstublieft %s om deze service te blijven gebruiken. + Gelieve %s om deze limiet te verhogen. + Gelieve %s om deze dienst te blijven gebruiken. Foutmelding Status.im-thema Toch bellen - Accepteren + Aanvaarden - Lees en accepteer het beleid van deze thuisserver: + Gelieve het beleid van deze thuisserver te lezen en aanvaarden: Oproepen - Gebruik de standaardbeltoon van Riot voor binnenkomende oproepen + Gebruik de standaardbeltoon van Riot voor inkomende oproepen Beltoon voor inkomende oproepen Selecteer beltoon voor oproepen: Eruit sturen - Weet je zeker dat je deze gebruiker dit gesprek uit wil sturen? - Weet je zeker dat je deze gebruikers dit gesprek uit wil sturen? + Weet u zeker dat u deze gebruiker uit dit gesprek wilt sturen\? + Weet u zeker dat u deze gebruikers uit dit gesprek wilt sturen\? Reden Versie %s - Preview van links in het gesprek tonen (als je thuisserver deze functie ondersteunt). - Stuur typnotificaties - Laat andere gebruikers weten dat je aan het typen bent. + Voorvertoning van koppelingen in het gesprek tonen (als uw thuisserver deze functie ondersteunt). + Typmeldingen versturen + Laat andere gebruikers weten dat u aan het typen bent. Markdown-opmaak - Berichten opmaken met Markdownsyntax voordat ze verstuurd worden. Hiermee kun je uitgebreide opmaak gebruiken, zoals sterretjes voor schuingedrukte tekst. - Laat leesbevestigingen zien - Klik op de leesbevestigingen voor een uitgebreide lijst. - Laat toetredingen en verlatingen zien - Meldingen over uitnodigingen, kicks en bans worden altijd weergegeven. - Laat accountgebeurtenissen zien + Maak berichten op met Markdown-syntax voordat ze verstuurd worden. Hiermee kunt u uitgebreide opmaak gebruiken, zoals sterretjes voor schuingedrukte tekst. + Leesbevestigingen weergeven + Tik op de leesbevestigingen voor een uitgebreide lijst. + Toetredingen en verlatingen weergeven + Meldingen over uitnodigingen, verwijderingen en verbanningen worden altijd weergegeven. + Accountgebeurtenissen weergeven Omvat veranderingen in avatar en weergavenaam. + Dienst wordt geïnitialiseerd + Sleutelback-up + Sleutelback-up gebruiken + + Sleutelback-up is nog niet klaar, even geduld… + Indien u zich nu afmeldt, zult u uw versleutelde berichten verliezen + Sleutelback-up is bezig. Indien u zich nu afmeldt, zult u de toegang tot uw versleutelde berichten verliezen. + Veilige sleutelback-up dient actief te zijn op al uw apparaten om de toegang tot uw versleutelde berichten niet te verliezen. + Ik wil mijn versleutelde berichten niet + Sleutels worden geback-upt… + Sleutelback-up gebruiken + Weet u het zeker\? + Back-up maken + U zult de toegang tot uw versleutelde berichten verliezen, tenzij u eerst een back-up van uw sleutels maakt vooraleer u zich afmeldt. + + Blijven + Overslaan + Klaar + Afbreken + Negeren + + Weet u zeker dat u zich wilt afmelden\? + Markeren als gelezen + Aanmelden met unieke aanmelding + Deze URL kan niet bereikt worden, gelieve deze na te kijken + Uw apparaat gebruikt een verouderd TLS-beveiligingsprotocol, dat kwetsbaar is voor aanvallen. Uit veiligheidsoverwegingen zult u geen verbinding kunnen maken + Video-oproep gaande… + + Geavanceerde meldingsinstellingen + Stel het belang van meldingen in op evenement, en configureer geluiden, LED’s en vibratie + Meldingsbelang op evenement + + Problemen met meldingen oplossen + Diagnostische probleemoplossingsinformatie + Testen uitvoeren + Bezig met uitvoeren… (%1$d van %2$d) + Basisdiagnose is oké. Als u nog steeds geen meldingen ontvangt, gelieve dan een bugmelding in te dienen om ons te helpen onderzoeken. + Er zijn één of meer tests mislukt, probeer de aanbevolen oplossing(en). + Er zijn één of meer tests mislukt, gelieve een bugmelding in te dienen om ons te helpen onderzoeken. + + Systeeminstellingen. + Meldingen zijn ingeschakeld in de systeeminstellingen. + Meldingen zijn uitgeschakeld in de systeeminstellingen. +\nGelieve deze te controleren. + Instellingen openen + + Accountinstellingen. + Meldingen zijn ingeschakeld voor uw account. + Meldingen zijn uitgeschakeld voor uw account. +\nGelieve de accountinstellingen te controleren. + Inschakelen + + Apparaatinstellingen. + Meldingen zijn ingeschakeld voor dit apparaat. + Meldingen zijn niet toegestaan voor dit apparaat. +\nGelieve de Riot-instellingen te controleren. + Inschakelen + + Aangepaste instellingen. + Sommige soorten berichten zijn stil (ze geven een geluidsloze melding). + Sommige meldingen zijn uitgeschakeld in uw aangepaste instellingen. + Laden van aangepaste regels is mislukt, probeer het opnieuw. + Instellingen controleren + + Play-diensten controleren + De APK van Google Play Services is beschikbaar en up-to-date. + Riot maakt gebruikt van Google Play Services om pushberichten af te leveren, maar dit lijkt niet juist geconfigureerd te zijn: +\n%1$s + Play-diensten herstellen + + Firebase-bewijs + Het FCM-bewijs is opgehaald: +\n%1$s + Het FCM-bewijs is niet opgehaald: +\n%1$s + [%1$s] +\nDeze fout is onafhankelijk van Riot. Volgens Google betekent deze fout dat het apparaat te veel apps heeft geregistreerd met FCM. De fout treedt enkel op ingeval er een enorm aantal apps is, dus zou dit de gemiddelde gebruiker niet mogen hinderen. + [%1$s] +\nDeze fout is onafhankelijk van Riot. Ze kan verschillende oorzaken hebben. Misschien werkt het als u het later opnieuw probeert. U kunt ook controleren of het gegevensverbruik van Google Play Services niet wordt beperkt in de systeeminstellingen, of dat de klok van uw apparaat wel juist staat, of dat het misschien aan een aangepaste ROM ligt. + [%1$s] +\nDeze fout is onafhankelijk van Riot. Er is geen Google-account verbonden met de telefoon. Open het accountbeheer en voeg er een Google-account toe. + Account toevoegen + + Bewijsregistratie + FCM-bewijs geregistreerd bij thuisserver. + FCM-bewijs niet geregistreerd bij thuisserver: +\n%1$s + + Meldingsdienst + Meldingsdienst is actief. + Meldingsdienst is niet actief. +\nProbeer de app te herstarten. + Dienst starten + + Meldingsdienst automatisch herstarten + Dienst is afgesloten en automatisch herstart. + Dienst is niet herstart + + Starten bij opstarten van apparaat + De dienst zal starten wanneer het apparaat wordt herstart. + De dienst zal niet starten wanneer het apparaat wordt herstart en u zult geen meldingen ontvangen tot u Riot hebt geopend. + Starten bij opstarten inschakelen + + Achtergrondbeperkingen controleren + Achtergrondbeperkingen zijn uitgeschakeld voor Riot. Deze test dient uitgevoerd te worden met een mobiele verbinding (geen wifi). +\n%1$s + Achtergrondbeperkingen zijn ingeschakeld voor Riot. +\nAl wat de app probeert te doen zal in de achtergrond hevig beperkt worden; dit kan het correct functioneren van meldingen beïnvloeden. +\n%1$s + Beperkingen uitschakelen + + Accuoptimalisatie + Riot wordt niet beperkt door accuoptimalisatie. + Als een gebruiker een apparaat los van de oplader een tijd laat stilliggen, met het scherm uitgeschakeld, gaat het apparaat in slaapmodus. Dit verhindert apps de toegang tot het netwerk, en stelt hun taken, synchronisaties en standaardalarmen uit. + Optimalisatie negeren + + De app heeft geen verbinding met de homeserver nodig in de achtergrond, dit zou accuverbruik moeten verlagen + Lawaaiierige meldingen configureren + Oproepmeldingen configureren + Stille meldingen configureren + Bepaal de LED-kleur, vibratie, geluid, … + + + Beheer van cryptografische sleutels + Berichten versturen met Enter + De Enter-knop van het toetsenbord zal berichten versturen in plaats van een regeleinde in te voegen + + Achtergrondverbinding + Riot heeft een achtergrondverbinding met lage impact nodig om betrouwbare meldingen te kunnen hebben. +\nOp het volgende scherm zult u gevraagd worden om Riot toestemming te verlenen om altijd in de achtergrond te kunnen draaien, gelieve deze toestemming te verlenen. + Toestemming verlenen + + Databesparingsmodus past een specifieke filter toe zodat aanwezigheidsupdates en typmeldingen weggefilterd worden. + + Er is een fout opgetreden bij het verifiëren van uw e-mailadres. + + Wachtwoord + Wachtwoord bijwerken + Het wachtwoord is ongeldig + Wachtwoorden komen niet overeen + + Er is een fout opgetreden bij het verifiëren van uw telefoonnummer. + Bijkomende info: %s + + Media + Standaardcompressie + Kiezen + Standaardmediabron + Kiezen + Sluitergeluid afspelen + + Maak een wachtwoord aan om de geëxporteerde sleutels mee te versleutelen. U heeft dit wachtwoord nodig om de sleutels te kunnen importeren. + Herstel van versleutelde berichten + Sleutelback-up beheren + + %1$d/%2$d sleutel(s) geïmporteerd. + + + %1$s: 1 bericht + %1$s: %2$d berichten + + + %d melding + %d meldingen + + + Nieuwe gebeurtenis + Kamer + Nieuwe berichten + Nieuwe uitnodiging + Ik + ** Versturen mislukt - open de kamer + + Start de systeemcamera in plaats van het aangepaste camerascherm. + Deze optie vereist een externe app om de berichten mee op te nemen. + + De opdracht ‘%s’ heeft meer parameters nodig, of sommige parameters zijn onjuist. + Markdown is ingeschakeld. + Markdown is uitgeschakeld. + + Stil + Voer een gebruikersnaam in. + Kamerleden lui laden + Verbeter de prestaties door kamerleden enkel bij de eerste weergave te laden. + Uw thuisserver ondersteunt het lui laden van kamerleden nog niet. Probeer het later opnieuw. + + Sorry, er is een fout opgetreden + + uitvouwen + invouwen + + Infogebied weergeven + Altijd + Voor berichten en fouten + Enkel voor fouten + + %1$s: + %1$s: %2$s + +%d + %d+ + Er is geen geldige APK van Google Play Services gevonden. Meldingen zullen mogelijk niet correct functioneren. + + Riot.im - Communiceer op uw manier + We blijven Riot.im voortdurend aanpassen en verbeteren. Het volledige wijzigingslogboek vindt u hier: %1$s. Om niets te missen, houdt u best uw updates ingeschakeld. + Een universele en veilige chat-app, volledig onder uw controle. + Een chat-app, onder uw controle en heel flexibel. Riot laat u communiceren zoals u dat wilt. Gemaakt voor [matrix] - de standaard voor open, gedecentraliseerde communicatie. +\n +\nMaak een gratis account aan op matrix.org, verkrijg uw eigen server op https://modular.im, of gebruik een andere Matrix-server. +\n +\nWaarom zou ik voor Riot.im kiezen\? +\n +\n• VOLLEDIGE COMMUNICATIE: maak kamers aan rond uw teams, uw vrienden, uw gemeenschap - hoe u maar wilt! Chat, deel bestanden, voeg widgets toe en maak stem- en video-oproepen - allemaal volledig gratis. +\n +\n• KRACHTIGE INTEGRATIE: gebruik Riot.im met de hulpmiddelen waarmee u vertrouwd bent. Met Riot.im kunt u zelfs chatten met gebruikers en groepen op andere chat-apps. +\n +\n• PRIVÉ EN VEILIG: houd uw gesprekken geheim. Eind-tot-eind-versleuteling van de bovenste plank zorgt ervoor dat uw privécommunicatie ook privé blijft. +\n +\n• OPEN, NIET GESLOTEN: vrije software, gebouwd op Matrix. Wees baas over uw eigen gegevens door uw eigen server te gebruiken, of te kiezen voor een andere server die u vertrouwt. +\n +\n• WAAR U OOK BENT: houd contact waar u ook bent met volledig gesynchroniseerde berichtgeschiedenis op al uw apparaten, en online op https://riot.im. + + Wachtwoord aanmaken + Wachtwoorden komen niet overeen + Voer een wachtwoord in + Wachtwoord is te zwak + + Verwijder het wachtwoord als u wilt dat Riot een herstelsleutel genereert. + Geen Matrix-sessie beschikbaar + + Verlies nooit uw versleutelde berichten + Berichten in versleutelde kamers worden beveiligd met eind-tot-eind-versleuteling. Enkel de ontvanger(s) en u hebben de sleutels om deze berichten te lezen. +\n +\nMaak een veilige back-up van uw sleutels om ze niet te verliezen. + Begin sleutelback-up te gebruiken + (Geavanceerd) + Sleutels handmatig exporteren + + Beveilig uw back-up met een wachtwoord. + We bewaren een versleutelde kopie van uw sleutels op onze thuisserver. Bescherm uw back-up met een wachtwoord om deze veilig te houden. +\n +\nVoor een maximale beveiliging zou deze sleutel moeten verschillen van uw accountwachtwoord. + Wachtwoord instellen + Back-up wordt aangemaakt + Of beveilig uw back-up met een herstelsleutel, en bewaar deze op een veilige plaats. + (Geavanceerd) Instellen met herstelsleutel + Klaar! + Uw sleutels worden geback-upt. + Uw herstelsleutel is een veiligheidsnet - u kunt deze gebruiken om de toegang tot uw versleutelde berichten te herstellen indien u uw wachtwoord vergeet. +\nBewaar uw herstelsleutel op een heel veilige plaats, zoals een wachtwoordbeheerder (of een kluis) + Bewaar uw herstelsleutel op een heel veilige plaats, zoals een wachtwoordbeheerder (of een kluis) + Klaar + Ik heb een kopie gemaakt + Herstelsleutel opslaan + Delen + Opslaan als bestand + De herstelsleutel is opgeslagen naar ‘%s’. +\n +\nLet op: dit bestand kan verwijderd worden als de app wordt verwijderd. + + Gelieve er een kopie van te maken + Herstelsleutel delen met… + Herstelsleutel wordt gegenereerd met wachtwoord, dit proces kan enkele seconden duren. + Herstelsleutel + Onverwachte fout + Back-up begonnen + Uw versleutelingssleutels worden nu in de achtergrond naar uw thuisserver geback-upt. De initiële back-up kan enkele minuten duren. + + + Weet u het zeker\? + U kunt de toegang tot uw berichten verliezen indien u zich afmeldt of dit apparaat verliest. + + Back-upversie wordt opgehaald… + Gebruik uw herstelwachtwoord om uw versleutelde berichtgeschiedenis te ontgrendelen + uw herstelsleutel gebruiken + Als u uw herstelwachtwoord niet meer weet, kunt u %s. + + Gebruik uw herstelsleutel om uw versleutelde berichtgeschiedenis te ontgrendelen + Voer de herstelsleutel in + + Berichtherstel + + Herstelsleutel verloren\? U kunt er een nieuwe instellen in de instellingen. + De back-up kan met dit wachtwoord niet ontsleuteld worden: controleer of u het juiste herstelwachtwoord heeft ingevoerd. + Netwerkfout: controleer uw verbinding en probeer het opnieuw. + + Back-up wordt hersteld: + Herstelsleutel wordt berekend… + Sleutels worden gedownload… + Sleutels worden geïmporteerd… + Geschiedenis ontgrendelen + Voer een herstelsleutel in + De back-up kan met deze herstelsleutel niet ontsleuteld worden: controleer of u de juiste herstelsleutel heeft ingevoerd. + + Back-up hersteld %s! + %1$d sessiesleutels hersteld, en %2$d nieuwe sleutel(s) die dit apparaat nog niet kende toegevoegd + + Back-up met %d sleutel hersteld. + Back-up met %d sleutels hersteld. + + + Er is %d nieuwe sleutel toegevoegd aan dit apparaat. + Er zijn %d nieuwe sleutels toegevoegd aan dit apparaat. + + + Verkrijgen van laatste herstelsleutelversie (%s) mislukt. + Sessieversleuteling is niet actief + + + Herstellen uit back-up + Back-up verwijderen + + Sleutelback-up is correct ingesteld voor dit apparaat. + Sleutelback-up is niet actief op dit apparaat. + Uw sleutels worden niet geback-upt vanaf dit apparaat. + + De back-up heeft een ondertekening van een onbekend apparaat met ID %s. + De back-up heeft een geldige ondertekening van dit apparaat. + De back-up heeft een geldige ondertekening van het geverifieerde apparaat %s. + De back-up heeft een geldige ondertekening van het ongeverifieerde apparaat %s + De back-up heeft een ongeldige ondertekening van het geverifieerde apparaat %s + De back-up heeft een ongeldige ondertekening van het ongeverifieerde apparaat %s + Verkrijgen van vertrouwensinformatie voor back-up mislukt (%s). + + Herstel nu met uw wachtwoord of herstelsleutel om sleutelback-up op dit apparaat te gebruiken. + Back-up wordt verwijderd… + Verwijderen van back-up is mislukt (%s) + + Back-up verwijderen + Uw geback-upte versleutelingssleutels verwijderen van de server\? U zult uw herstelsleutel niet meer kunnen gebruiken om de versleutelde berichtgeschiedenis te lezen. + + Nieuwe sleutelback-up + Er is een nieuwe sleutelback-up voor versleutelde berichten gedetecteerd. +\n +\nAls u deze nieuwe herstelmethode niet heeft ingesteld, is het mogelijk dat er een aanvaller toegang tot uw account probeert te verkrijgen. Wijzig onmiddellijk uw accountwachtwoord en stel een nieuwe herstelmethode in in de instellingen. + Ik was het + Verlies nooit uw versleutelde berichten + Begin sleutelback-up te gebruiken + + Verlies nooit uw versleutelde berichten + Sleutelback-up gebruiken + + Nieuwe sleutels voor versleutelde berichten + Beheren in sleutelback-up + + Back-up van sleutels wordt gemaakt… + + Alle sleutels zijn geback-upt + + Back-up van %d sleutel wordt gemaakt… + Back-up van %d sleutels wordt gemaakt… + + + Versie + Algoritme + Ondertekening + + Ongeldig thuisserverontdekkingsantwoord + Serveropties automatisch aanvullen + Riot heeft een aangepaste serverconfiguratie gedetecteerd voor uw gebruikers-ID-domein ‘%s’: +\n%s + Configuratie gebruiken + diff --git a/vector/src/main/res/values-pl/strings.xml b/vector/src/main/res/values-pl/strings.xml index e4e1f378..8ed29953 100644 --- a/vector/src/main/res/values-pl/strings.xml +++ b/vector/src/main/res/values-pl/strings.xml @@ -59,10 +59,10 @@ Ludzie Pokoje - Szukaj pokojów - Szukaj w ulubionych - Szukaj ludzi - Szukaj pokojów + Filtruj nazwy pokojów + Filtruj ulubione + Filtruj ludzi + Filtruj nazwy pokojów Zaproszenia Niski priorytet @@ -105,7 +105,7 @@ Dołącz do pokoju Nazwa użytkownika - Zarejestruj + Stwórz konto Zaloguj się Wyloguj Adres serwera @@ -123,7 +123,7 @@ Zrób zdjęcie lub film Zaloguj się - Zarejestruj + Stwórz konto Wyślij Pomiń Wyślij e-mail przywracający @@ -415,9 +415,9 @@ Wprowadź hasło, aby kontynuować. Ten numer telefonu jest już używany. Zmień hasło - Stare hasło + Bieżące hasło Nowe hasło - Potwierdź hasło + Potwierdź nowe hasło Nie udało się zmienić hasła Twoje hasło zostało zmienione Pokazywać wszystkie wiadomości od %s? @@ -556,7 +556,7 @@ Zauważ, że ta czynność spowoduje ponowne uruchomienie aplikacji i może to t Działania Społeczności - Odszukaj społeczności + Filtruj nazwy społeczności Zaproś Społeczności @@ -623,8 +623,8 @@ Zezwolić Riot na dostęp do kontaktów? Lista uczestników Otwarty nagłówek Synchronizacja… - Zaproszenie zostało wysłane do %s, który nie jest powiązany z zalogowanym kontem. -Możesz zalogować się z wykorzystaniem innego konta, albo dodać ten adres e-mail do swoich kontaktów. + Zaproszenie zostało wysłane do %s, który nie jest powiązany z zalogowanym kontem. +\nMożesz zalogować się z wykorzystaniem innego konta, albo dodać ten adres e-mail do swojego konta. Jeden aktywny członek Kilku aktywnych członków @@ -693,7 +693,7 @@ Jesteś pewien? Kraj książki adresowej Ekran domowy - Zezwól na domyślny podgląd zawartości URL + Podgląd zawartości URL Pokaż czas w formacie 12-godzinnym Wibruj gdy ktoś wspomni o tobie @@ -757,7 +757,7 @@ Jesteś pewien? Standardowa Zmniejszona prywatność Aplikacja wymaga uprawnień do działania w tle - • Powiadomienia wysyłane za pomocą Google Cloud Messaging + • Powiadomienia wysyłane za pomocą Firebase Cloud Messaging • Powiadomienia zawierają tylko meta-dane • Treść powiadomienia jest bezpiecznie pobierana z domowego serwera Matrix • Powiadomienia zawierają meta-dane oraz treść wiadomości @@ -1133,7 +1133,7 @@ Aby upewnić się, że niczego nie przegapisz, po prostu miej włączone aktuali Spróbuj uruchomić ponownie aplikację. W trakcie połączenia wideo… - Przywracanie wiadomości + Kopia Zapasowa Klucza Kopia zapasowa kluczy nie jest zakończona, proszę czekać… Pomiń Zaawansowane ustawienia powiadomień @@ -1145,4 +1145,20 @@ Spróbuj uruchomić ponownie aplikację. Przerwij Czy na pewno chcesz się wylogować? + Trwa tworzenie kopii zapasowej klucza. Jeśli wylogujesz się teraz utracisz dostęp do zaszyfrowanych wiadomości. + Nie chcę moich zaszyfrowanych wiadomości + Utracisz dostęp do zaszyfrowanych wiadomości, chyba że wykonasz kopię zapasową kluczy przed wylogowaniem się. + + Zostań + Ignoruj + + Domyślna kompresja + Konfiguruj głośne powiadomienia + Konfiguruj ciche powiadomienia + Ciche + Przywróć z kopii zapasowej + Usuń kopię zapasową + + Usuń kopię zapasową + Wersja diff --git a/vector/src/main/res/values-pt-rBR/strings.xml b/vector/src/main/res/values-pt-rBR/strings.xml index 793188b1..e1937505 100644 --- a/vector/src/main/res/values-pt-rBR/strings.xml +++ b/vector/src/main/res/values-pt-rBR/strings.xml @@ -1179,7 +1179,7 @@ Trabalhos que o aplicativo tentar fazer serão restringidos agressivamente enqua %d+ Nenhum APK do Google Play Services válido foi encontrado. Notificações podem não funcionar corretamente. - Riot.im - Comunique-se do seu jeito. + Riot.im - Comunique-se do seu jeito Um aplicativo de bate-papo universal seguro totalmente sob seu controle. Se um usuário deixar um dispositivo desconectado e parado por um período de tempo, com a tela desligada, o dispositivo entrará no modo Cochilo. Isso impede que os aplicativos acessem a rede e adiem seus trabalhos, sincronizações e alarmes padrão. Criar passphrase @@ -1247,4 +1247,16 @@ Para garantir que você não perca nada, mantenha suas atualizações ativadas." Seu dispositivo está usando um protocolo de segurança TLS desatualizado, vulnerável a ataques. Para sua segurança, você não poderá se conectar [%1$s] \nEste erro está fora de controle da Riot. Isso pode ocorrer por vários motivos. Talvez funcione se você tentar novamente mais tarde. Você também pode verificar se o Google Play Service não está restrito ao uso de dados nas configurações do sistema ou se o relógio do dispositivo está correto ou pode acontecer na ROM personalizada. + Inicializando o serviço + [%1$s]\nEste erro está fora de controle do Riot. Não há conta do Google no telefone. Por favor, abra o gerenciador de contas e adicione uma conta do Google. + Adicionar Conta + + Configurar notificações ruidosas + Configurar notificações de chamada + Configurar notificações silenciosas + Escolha a cor do LED, vibração, som… + + + Gerenciamento de Chaves de Criptografia + Enviar mensagem com a tecla enter diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml index eb100e38..ca9b07ca 100644 --- a/vector/src/main/res/values-ru/strings.xml +++ b/vector/src/main/res/values-ru/strings.xml @@ -32,9 +32,10 @@ Переименовать Пожаловаться на содержимое Активный вызов - Установлен конференц-вызов.\nПрисоединяйтесь с %1$s или %2$s. - голосом - видео + Ongoing conference call. +\nПрисоединиться как %1$s или %2$s + Голос + Видео Не удалось осуществить вызов, попробуйте позже "Из-за отсутствия разрешений некоторые функции могут быть недоступны.. Вам нужно разрешение на приглашение для начала конференции в этой комнате @@ -69,10 +70,10 @@ Комнаты - Поиск комнат - Поиск избранных - Поиск людей - Поиск комнат + Фильтр названия комнаты + Фильтр избраного + Фильтр людей + Фильтр названия комнаты Приглашения @@ -116,7 +117,7 @@ Войти в Комнату Логин - Зарегистрироваться + Создать аккаунт Войти Выйти URL сервера @@ -194,7 +195,7 @@ Email также позволит вам при необходимости во Сбой регистрации: сетевая ошибка Сбой регистрации Сбой регистрации: ошибка проверки email - Введите корректный URL + Пожалуйста, введите корректный URL Неверное имя пользователя или пароль Указанный токен доступа не распознан @@ -507,9 +508,9 @@ Email также позволит вам при необходимости во Этот номер телефона уже используется. Смена пароля - Cтарый пароль + Текущий пароль Новый пароль - Подтверждение пароля + Подтвердите новый пароль Не удалось обновить пароль Пароль был обновлен Отображать сообщения пользователя %s? @@ -1047,7 +1048,7 @@ Email также позволит вам при необходимости во Сейчас %1$s %1$s %2$s назад - "%1$s, " + "%1$S " %1$s и %2$s %1$s %2$s @@ -1091,7 +1092,7 @@ Email также позволит вам при необходимости во Из-за ежемесячного ограничения активных пользователей сервера некоторые из пользователей не смогут залогиниться. Сервер достиг ежемесячного ограничения активных пользователей. - Пожалуйста, %s, для увеличения этого лимита. + Пожалуйста %s для увеличения этого лимита. Пожалуйста, %s, чтобы продолжить пользоваться этим сервисом. Ленивая подгрузка собеседников @@ -1239,7 +1240,7 @@ Email также позволит вам при необходимости во %d+ Не найден APK сервисов Google Play. Уведомления могут работать неправильно. - Riot.im — общайся по-своему. + Riot.im — общайся по-своему Универсальное приложение для безопасного общения, полностью находящееся под вашим контролем. Не влияет на приглашения, исключения и запреты. "Приложение для чата, под вашим контролем и полностью гибкое. Riot позволяет вам общаться так, как вы хотите. Сделано на [matrix] — стандарт для открытого, децентрализованного общения. @@ -1453,4 +1454,36 @@ Email также позволит вам при необходимости во Вычисление ключа восстановления… Игнорировать + Инициализация сервиса + Отметить как прочитанное + Войти с помощью единого входа + "Этот URL не доступен, пожалуйста проверить " + Отправить сообщение нажав ввод + Обновить пароль + Пароль не действителен + Пароли не совпадают + + Медиа + Сжатия по умолчанию + Выберите + Источник медиа по умолчанию + Выберите + + %1$s: 1 сообщение + %1$s: %2$d сообщения + %1$s: %2$d сообщения + + + %d оповещение + %d оповещения + %d оповещения + + + Новое событие + Комната + Новые сообщения + Новое приглашение + Мне + Использовать настройку + diff --git a/vector/src/main/res/values-sk/strings.xml b/vector/src/main/res/values-sk/strings.xml index 84f2870b..a170ce31 100644 --- a/vector/src/main/res/values-sk/strings.xml +++ b/vector/src/main/res/values-sk/strings.xml @@ -35,7 +35,8 @@ Premenovať Ohlásiť obsah Aktívny hovor - Prebiehajúci konferenčný hovor.\nPripojte sa ako %1$s alebo %2$s. + Prebiehajúci konferenčný hovor. +\nPripojte sa ako %1$s alebo %2$s audio video Nie je možné začať hovor. Prosím, skúste to neskôr @@ -69,10 +70,10 @@ ľudia Miestnosti - Hľadať miestnosti - Hľadať obľúbené - Hľadať ľudí - Hľadať miestnosti + Filtrovať názvy miestností + Filtrovať obľúbené + Filtrovať ľudí + Filtrovať názvy miestností Pozvania Nízka priorita @@ -82,7 +83,7 @@ Adresár používateľov Len Matrix kontakty Žiadne konverzácie - Aplikácia Riot nemá udelené právo čítať lokálne kontakty + Aplikácii Riot ste nepovolili prístup k lokálnym kontaktom Žiadne výsledky Miestnosti @@ -115,7 +116,7 @@ Vstúpiť do miestnosti Meno používateľa - Registrovať + Vytvoriť účet Prihlásiť sa Odhlásiť sa Adresa domovského servera @@ -130,7 +131,7 @@ Poslať fotku alebo video Prihlásiť sa - Registrovať + Vytvoriť účet Odoslať Preskočiť Poslať obnovovací email @@ -179,7 +180,7 @@ Emailovú adresu si môžete k účtu pridať neskôr cez nastavenia. Ak chcete obnoviť vaše heslo, zadajte emailovú adresu prepojenú s vašim účtom: Musíte zadať emailovú adresu prepojenú s vašim účtom. Musíte zadať nové heslo. - Na adresu %s bola odoslaná správa. Potom, čo prejdete na odkaz z tejto správy, kliknite nižšie. + Na adresu %s bola odoslaná správa. Potom, čo prejdete na odkaz z tejto správy, pokračujte kliknutím nižšie. Nepodarilo sa overiť emailovú adresu: Uistite sa, že ste správne klikli na odkaz v emailovej správe Vaše heslo bolo obnovené. @@ -208,7 +209,7 @@ Ospravedlňujeme sa za spôsobené ťažkosti. Zoznam potvrdení o prečítaní - "Odoslať ako " + Odoslať ako Pôvodný Veľký Stredný @@ -281,8 +282,8 @@ Chcete aplikácii Riot povoliť prístup k vašim kontaktom? Preskočiť na prvú neprečítanú správu. Používateľ %s vás pozval vstúpiť do tejto miestnosti - Toto pozvanie bolo odoslané na emailovú adresu, ktorá nie je priradená k tomuto účtu: %s. -Môžete sa prihlásiť k inému účtu, alebo pridať emailovú adresu do práve prihláseného účtu. + Toto pozvanie bolo odoslané na emailovú adresu %s, ktorá nie je priradená k tomuto účtu. +\nMôžete sa prihlásiť k inému účtu, alebo pridať emailovú adresu do práve prihláseného účtu. Pokúšate sa zobraziť %s. Chcete vstúpiť a pridať sa k diskusii? miestnosť Toto je náhľad na miestnosť. Všetky akcie pre túto miestnosť sú zakázané. @@ -501,7 +502,7 @@ Ak chcete pokračovať, prosím zadajte vaše heslo. Zmeniť heslo Súčasné heslo Nové heslo - Potvrdiť heslo + Potvrdiť nové heslo Nepodarilo sa zmeniť heslo Úspešne ste si zmenili heslo Zobraziť všetky správy od používateľa %s? @@ -553,8 +554,8 @@ Pozor! Vykonaním tejto akcie reštartujete aplikáciu a opätovné načítanie Ak chcete vytvoriť odkaz do miestnosti, musíte najprv nastaviť jej adresu. Len pozvaní ľudia - Ktokoľvek, kto pozná odkaz do miestnosti (okrem hostí) - Ktokoľvek, kto pozná odkaz do miestnosti (vrátane hostí) + Ktokoľvek, kto pozná odkaz do miestnosti, okrem hostí + Ktokoľvek, kto pozná odkaz do miestnosti, vrátane hostí Používatelia, ktorým bol zakázaný vstup @@ -642,8 +643,7 @@ Pozor: tento súbor môže byť automaticky zmazaný po odinštalovaní aplikác Overiť zariadenie Ak chcete overiť, či toto zariadenie je skutočne dôverihodné, kontaktujte jeho vlastníka iným spôsobom (napr. osobne alebo cez telefón) a opýtajte sa ho, či kľúč, ktorý má pre toto zariadenie zobrazený v nastaveniach sa zhoduje s kľúčom zobrazeným nižšie: - Ak sa kľúče zhodujú, stlačte tlačidlo Overiť nižšie. Ak sa nezhodujú, niekto ďalší odpočúva toto zariadenie a v takomto prípade by ste asi mali namiesto toho stlačiť tlačidlo Pridať na čiernu listinu. -V budúcnosti plánujeme tento proces overovania zariadení zjednodušiť. + Ak sa kľúče zhodujú, stlačte tlačidlo Overiť nižšie. Ak sa nezhodujú, niekto ďalší odpočúva toto zariadenie a mali by ste ho pridať na čiernu listinu. V budúcnosti plánujeme tento proces overovania zariadení zjednodušiť. Overil som, kľúče sa zhodujú Riot od teraz podporuje E2E šifrovanie. Ak si ho želáte povoliť, musíte sa teraz odhlásiť a následne prihlásiť znovu. @@ -733,7 +733,7 @@ Neznáme zariadenia: Ukončiť Komunity - Hľadať komunity + Filtrovať názvy komunít Pozvanie Komunity @@ -871,7 +871,7 @@ Neznáme zariadenia: Nižšia úroveň súkromia Normálne Aplikácia vyžaduje povolenie bežať na pozadí - • Oznámenia sú zasielané prostredníctvom cloudových služieb Google + • Oznámenia sú zasielané prostredníctvom cloudových služieb spoločnosti Google • Oznámenia neobsahujú text správy, len meta údaje • Obsah správy oznámení je bezpečne prevzatý priamo z domovského servera Matrix • Oznámenia obsahujú meta údaje aj obsah správy @@ -886,8 +886,8 @@ Neznáme zariadenia: Odoslať nálepku Momentálne nemáte aktívne žiadne balíčky s nálepkami. - -Chcete si nejaké pridať teraz? +\n +\nChcete si nejaké pridať teraz\? Deaktivovať účet Deaktivovať môj účet @@ -1110,7 +1110,7 @@ Prosím, skontrolujte nastavenia Riot. Kontrola aplikácii Služby Google Play Aplikácia Služby Google Playje k dispozícii a aktualizovaná. Riot používa aplikáciu Služby Google Play na doručovanie oznámení. No zdá sa, že táto nie je správne nakonfigurovaná: -%1$s +\n%1$s Opraviť Služby Play Firebase Token @@ -1149,7 +1149,7 @@ Prosím skúste reštartovať aplikáciu. Optimalizácia batérie Chod Riot nie je ovplyvnený nastavením optimalizácie batérie. - "Ak používateľ na nejaký čas ponechá zariadenie s vypnutou obrazovkou odložené odpojené od napájania, na zariadení sa použije režim Doze. Toto aplikáciám zabráni pristupovať k sieti, pozastaví ich naplánované úlohy, synchronizáciu aj bežné signály. " + Ak používateľ na nejaký čas ponechá zariadenie s vypnutou obrazovkou odložené odpojené od napájania, na zariadení sa použije režim Doze. Toto aplikáciám zabráni pristupovať k sieti, pozastaví ich naplánované úlohy, synchronizáciu aj bežné signály. Ignorovať optimalizáciu Zobraziť náhľady odkazov v konverzáciách (ak sú podporované domovským serverom). @@ -1185,11 +1185,11 @@ Na ďalšej obrazovke vás systém požiada o povolenie vždy bežať na pozadí Nenájdená aktívna aplikácia Služby Google Play. Je možné, že nebude správne fungovať doručovanie oznámení. - Riot.im - otvorená spolupráca pre tímy + Riot.im - Komunikujte, Podľa seba "Riot.im neustále aktualizujeme s vylepšeniami a zmenami. Podrobný zoznam zmien (anglicky) nájdete na adrese: %1$s. Aby ste nič nezmeškali, nevypínajte prosím automatické aktualizácie." - Vitajte v aplikácii Riot.im: v novom svete otvorenej komunikácii! + Univerzálna a bezpečná aplikácia na okamžitú komunikáciu úplne pod vašou kontrolou. Riot.im je jednoduché a elegantné prostredie určené na vzájomnú spoluprácu, ktoré združuje vaše rôzne konverzácie a integrácie do jedinej aplikácii. Postavený nad skupinovými konverzáciami, Riot.im vám umožňuje zdieľať správy, obrázky, videá aj akékoľvek súbory - pracovať s vlastnými nástrojmi a pristupovať ku všetkým vašim komunitám pod jednou strechou. S jednou jedinou totožnosťou si vystačíte pre všetky vaše tímy: nie je potrebné prepínať sa medzi viacerými účtami, môžete komunikovať s ľuďmi s rôznych organizácií vo verejných alebo súkromných miestnostiach: od profesionálnych projektov až po školské výlety, Riot.im sa stane centrom všetkych vašich diskusií! @@ -1221,4 +1221,95 @@ Pre vývojárov: Objavte skutočne výkonné možnosti otvorenej spolupráce s Riot.im! + Inicializácia služby + Zálohovanie kľúčov + Obnoviť kľúče zo zálohy + + Zálohovanie kľúčov nie je dokončené, prosím čakajte… + Ak sa teraz odhlásite, prídete o zašifrované správy + Prebieha zálohovanie kľúčov. Ak sa teraz odhlásite, prídete o zašifrované správy. + Bezpečné zálohovanie kľúčov by ste mali mať aktivované na všetkých zariadeniach, aby ste neprišli o prístup k zašifrovaným správam. + Nezáleží mi na zašifrovaných správach + Zálohovanie kľúčov… + Použiť zálohovanie kľúčov + Ste si istí\? + Zálohovať + Skôr než sa odhlásite, zálohujte si šifrovacie kľúče, inak prídete o prístup k zašifrovaným správam. + + Zostať + Vynechať + Hotovo + Prerušiť + Ignorovať + + Ste si istí, že sa chcete odhlásiť\? + Označiť ako prečítané + Prihlásiť sa použitím jediného prihlásenia + Na tejto adrese nie je dostupný žiadny obsah. Prosím, skontrolujte jej správnosť + Vaše zariadenie používa zastaralú verziu protokolu TLS, ktorá je náchylná na zraniteľnosti. Z dôvodu zachovania maximálnej bezpečnosti sa nebudete môcť pripojiť + Pokročilé nastavenia oznámení + Nastavenie dôležitosti oznámení pre udalosti, konfigurácia zvukových, vibračných a LED upozornení + Dôležitosť oznámení pre udalosti + + Vlastné nastavenia. + Pozor, Pre typi udalostí, ktoré majú dôležitosť nastavenú na Tiché, sa zobrazí oznámenie bez zvukového upozornenia. + Niektoré oznámenia máte zakázané v rozšírených nastaveniach. + Nepodarilo sa načítať pravidlá oznámení. Prosím, skúste znovu. + Skontrolovať nastavenia + + [%1$s] +\nNa zariadení máte množstvo aplikácií zaregistrovaných na doručovanie okamžitých oznámení cez služby Google play. Konfigurácia Riot nemá vplyv na výskyt tejto chyby. Podľa Google sa môže vyskytovať len pri veľmi vysokom počte nainštalovaných aplikácií. Bežní používatelia by týmto nemali byť postihnutí. + [%1$s] +\nKonfigurácia Riot nemá vplyv na zobrazenie tejto chyby. Táto chyba sa môže zobraziť z niekoľkých dôvodov. Uistite sa že máte správne nastavený systémový čas a že ste v nastaveniach systému aplikácii služby Google play neobmedzili používanie prístupu na internet. Chyba sa tiež môže zobrazovať na vlastných zostaveniach (ROM), alebo sa chyba môže samovoľne prestať zobrazovať neskôr. + [%1$s] +\nV zariadení nemáte nastavený účet Google. Prosím, pridajte si účet cez správcu účtov. Konfigurácia Riot nemá vplyv na zobrazenie tejto chyby. + Pridať účet + + Aplikácia sa nepotrebuje pripájať k domovskému serveru, keď beží na pozadí, čo môže predĺžiť výdrž batérie. + Nastaviť hlučné oznámenia + Nastaviť oznámenia prichádzajúceho hovoru + Nastaviť tiché oznámenia + Vybrať farbu upozornení LED, vibrácie, zvuky… + + + Správa šifrovacích kľúčov + Odosielať správy enterom + Stlačením klávesu enter na dotykovej klávesnici odošlete správu namiesto odriadkovania + + Režim šetrenia údajov aplikuje filter, ktorý potlačí aktualizácie prítomnosti a oznámenia pri písaní. + + Aktualizovať heslo + Heslo nie je správne + Heslá sa nezhodujú + + Médiá + Predvolená kompresia + Vybrať + Predvolený zdroj médií + Vybrať + Prehrať zvuk spúšte fotoaparátu + + Obnovenie zašifrovaných správ + Spravovať zálohovanie kľúčov + + + %1$s: 1 správa + %1$s: %2$d správy + %1$s: %2$d správ + + + %d oznámenie + %d oznámenia + %d oznámení + + + Nová udalosť + Miestnosť + Nové správy + Nové pozvanie + Ja + ** Nepodarilo sa odoslať - otvorte prosím miestnosť + + Tiché + Prosím zadajte meno používateľa. diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index 287734b9..4a420001 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -1,7 +1,7 @@ sq - AL + SQ Temë e Çelët Temë e Errët @@ -80,7 +80,7 @@ Ftesa Me përparësi të ulët - Sinjalizime sistemi + Sinjalizime Sistemi Biseda Libër adresash vendor @@ -189,7 +189,7 @@ Listë Grupesh - "Dërgoje si " + Dërgoje si Origjinale E madhe Mesatare @@ -299,7 +299,7 @@ Ftoni sipas ID-je KONTAKTE VENDORE (%d) - DREJTORI PËRDORUESI (%s) + DREJTORI PËRDORUESISH (%s) Vetëm përdorues të Matrix-it Ftoni përdorues sipas ID-je @@ -542,7 +542,7 @@ Fshehtëzimi Skaj-më-Skaj është aktiv Fshehtëzoje vetëm për pajisje të verifikuara Kjo dhomë s’ka adresa vendore - Adresë e re (p.sh. #foo:matrix.org) + Adresë e re (p.sh., #foo:matrix.org) ID bashkësie e pavlefshme \'%s\' s’është ID i vlefshëm për një bashkësi @@ -660,7 +660,7 @@ Markdown është çaktivizuar. Off - I zhurmshëm + Të zhurmshëm Mesazh i fshehtëzuar @@ -730,12 +730,13 @@ %d+ Po përgjohet për akte - Thirrje konference që po zhvillohet.\nMerrni pjesë me %1$s ose %2$s. - - video + Thirrje konference që po zhvillohet. +\nMerrni pjesë me %1$s ose %2$s + + Video Ju duhen leje për ftesa, që të nisni një konferencë në këtë dhomë Të parapëlqyer - S’e lejuat Riot-i të hyjë në kontaktet tuaja vendore + S’e lejuat Riot-in të hyjë në kontaktet tuaja vendore Ju lutemi, përshkruajeni të metën. Ç’po bënit? Ç’prisnit të ndodhte? Ç’ndodhi në fakt? Duket se po përplasni telefonin nga inati. Do të donit të hapej skena për njoftim të metash? Herën e fundit aplikacioni u vithis. Do të donit të hapej skena e raportimit të vithisjeve? @@ -746,18 +747,18 @@ Shtoni te llogaria juaj një adresë email, për t’u dhënë mundësinë përdoruesve t’ju zbulojnë dhe që t’ju lejojë të ricaktoni fjalëkalimin. Shtoni te llogaria juaj një adresë email dhe/ose një numër telefoni, për t’u dhënë mundësinë përdoruesve t’ju zbulojnë. - -Adresa email do t’ju lejojë edhe të ricaktoni fjalëkalimin tuaj. +\n +\nAdresa email do t’ju lejojë edhe të ricaktoni fjalëkalimin tuaj. Shtoni te llogaria juaj një adresë email dhe një numër telefoni, për t’u dhënë mundësinë përdoruesve t’ju zbulojnë. - -Adresa email do t’ju lejojë edhe të ricaktoni fjalëkalimin tuaj. +\n +\nAdresa email do t’ju lejojë edhe të ricaktoni fjalëkalimin tuaj. Regjistrimi me email dhe me numër telefoni njëherazi nuk mbulohet ende, deri sa të ketë API. Do të merret parasysh vetëm numri i telefonit. - -Email-in tuaj mund ta shtoni te profili juaj, te rregullimet. +\n +\nEmail-in tuaj mund ta shtoni te profili juaj, te rregullimet. Verifikimi i adresës email dështoi: sigurohuni se keni klikuar lidhjen te email-i Fjalëkalimi juaj u ri caktua. - -Është bërë dalja juaj nga llogaria në krejt pajisjet dhe s’do të merrni më njoftime push. Për riaktivizim të njoftimeve, ribëni hyrjen në çdo pajisje. +\n +\nËshtë bërë dalja juaj nga llogaria në krejt pajisjet dhe s’do të merrni më njoftime push. Për riaktivizim të njoftimeve, ribëni hyrjen në çdo pajisje. Ju lutemi, niseni Riot-in në një tjetër pajisje që mundet të shfshehtëzojë mesazhin, që kështu të mund të dërgojë kyçet te kjo pajisje. @@ -785,16 +786,17 @@ Email-in tuaj mund ta shtoni te profili juaj, te rregullimet. ID bashkësie të re (p.sh., +foo:matrix.org) Aktivizo fshehtëzim -(kujdes: s’mund të çaktivizohet më!) +\n(kujdes: s’mund të çaktivizohet më!) Riot tani mbulon fshehtëzim skaj-më-skaj, por lypset të ribëni hyrjen që ta aktivizoni. - -Mund ta bëni tani ose më vonë, që prej rregullimeve të aplikacionit. +\n +\nMund ta bëni tani ose më vonë, që prej rregullimeve të aplikacionit. Kjo dhomë përmban pajisje të panjohura që s’janë verifikuar. -Kjo do të thotë se nuk ka garanci se pajisjet u përkasin përdoruesve që pretendojnë se u përkasin. -Përpara se të vazhdoni, këshillojmë që të kaloni në proces verifikimi çdo pajisje, por mund të ridërgoni mesazhin pa verifikuar gjë, nëse parapëlqeni kështu. -Pajisje të panjohura: +\nKjo do të thotë se nuk ka garanci se pajisjet u përkasin përdoruesve që pretendojnë se u përkasin. +\nPërpara se të vazhdoni, këshillojmë që të kaloni në proces verifikimi çdo pajisje, por mund të ridërgoni mesazhin pa verifikuar gjë, nëse parapëlqeni kështu. +\n +\nPajisje të panjohura: Shtypni një shërbyes home që të paraqiten dhoma publike prej tij Ju duhen leje për të administruar widget-e në këtë dhomë @@ -828,8 +830,8 @@ Pajisje të panjohura: Në qoftë e mundur, ju lutemi, përshkrimin shkruajeni në anglisht. Hëpërhë, s’keni të aktivizuar ndonjë pako ngjitësash. - -Të shtohen ca tani? +\n +\nTë shtohen ca tani\? Na ndjeni, s’u gjet aplikacion i jashtëm për të plotësuar këtë veprim. @@ -848,27 +850,25 @@ Të shtohen ca tani? Po bëhet lidhja e thirrjes… Ana e largët dështoi të përgjigjet. Për të dërguar dhe ruajtur bashkëngjitje, Riot-i lyp leje të përdorë mediatekën tuaj. - - - -Ju lutemi, lejoni përdorimin, që nga flluska pasuese, që të jetë në gjendje të dërgojë kartela që nga telefoni juaj. +\n +\nJu lutemi, lejoni përdorimin, që nga flluska pasuese, që të jetë në gjendje të dërgojë kartela që nga telefoni juaj. Për të bërë foto dhe thirrje video, Riot-i lyp leje të përdorë kamerën tuaj. - - -Ju lutemi, lejoni përdorimin, që nga flluska pasuese, që të jetë në gjendje të bëjë thirrjen. + " +\n +\nJu lutemi, lejoni përdorimin, që nga flluska pasuese, që të jetë në gjendje të bëjë thirrjen." Për të kryer thirrje audio, Riot-i lyp leje të përdorë mikrofonin tuaj. - - -Ju lutemi, lejoni përdorimin, që nga flluska pasuese, që të jetë në gjendje të bëjë thirrjen. + " +\n +\nJu lutemi, lejoni përdorimin, që nga flluska pasuese, që të jetë në gjendje të bëjë thirrjen." Për të kryer thirrje video, Riot-i lyp leje të përdorë kamerën dhe mikrofonin tuaj. - -Ju lutemi, lejoni përdorimin, që nga flluskat pasuese, që të jetë në gjendje të bëjë thirrjen. +\n +\nJu lutemi, lejoni përdorimin, që nga flluskat pasuese, që të jetë në gjendje të bëjë thirrjen. Për të gjetur përdorues të tjerë Matrix, bazuar në email-et apo numrat e tyre të telefonit, Riot-i lyp leje të përdorë kontaktet e librit tuaj të adresave. - -Ju lutemi, lejoni përdorimin, që nga flluska pasuese, që të zbulohen te libri i adresave përdorues të kapshëm që nga Riot. +\n +\nJu lutemi, lejoni përdorimin, që nga flluska pasuese, që të zbulohen te libri i adresave përdorues të kapshëm që nga Riot. Për të gjetur përdorues të tjerë Matrix, bazuar në email-et apo numrat e tyre të telefonit, Riot-i lyp leje të përdorë kontakte nga libri juaj i adresave. - -Të lejohet Riot-i të hyjë në kontaktet tuaja? +\n +\nTë lejohet Riot-i të hyjë në kontaktet tuaja\? Na ndjeni. Veprimi nuk u krye, për shkak lejesh që mungojnë @@ -882,11 +882,11 @@ Të lejohet Riot-i të hyjë në kontaktet tuaja? 1 anëtar %d anëtarë - Të shfaqen krejt mesazhet nga ky përdorues? - -Kini parasysh që ky veprim do të sjellë rinisjen e aplikacionit dhe mund të hajë ca kohë. + Të shfaqen krejt mesazhet nga ky përdorues\? +\n +\nKini parasysh që ky veprim do të sjellë rinisjen e aplikacionit dhe mund të hajë ca kohë. S’do të jeni në gjendje ta zhbëni këtë ndryshim, ngaqë po e promovoni përdoruesin të ketë të njëjtën shkallë pushteti si ju vetë. -Jeni i sigurt? +\nJeni i sigurt\? Ju lutemi, jepni një ose më shumë adresa email ose ID Matrix Mesazhet s’u dërguan, për shkak të pranisë së pajisjeve të panjohura. %1$s ose %2$s tani? @@ -895,9 +895,9 @@ Jeni i sigurt? %d mesazhe të rinj - Doni të fshihen krejt mesazhet nga ky përdorues? - -Kini parasysh që ky veprim do të sjellë rinisjen e aplikacionit dhe mund të hajë ca kohë. + Doni të fshihen krejt mesazhet nga ky përdorues\? +\n +\nKini parasysh që ky veprim do të sjellë rinisjen e aplikacionit dhe mund të hajë ca kohë. 1 dhomë %d dhoma @@ -907,7 +907,7 @@ Kini parasysh që ky veprim do të sjellë rinisjen e aplikacionit dhe mund të U gjetën %1$s dhoma për %2$s Ky veprim lyp mirëfilltësim shtesë. -Që të vazhdohet, ju lutemi, jepni fjalëkalimin tuaj. +\nQë të vazhdohet, ju lutemi, jepni fjalëkalimin tuaj. Ju lutemi, kontrolloni email-in tuaj dhe klikoni mbi lidhjen që përmban. Pasi të jetë bërë kjo, klikoni që të vazhdohet. S’arrihet të verifikohet adresë email. Ju lutemi, kontrolloni email-in tuaj dhe klikoni mbi lidhjen që përmban. Pasi të jetë bërë kjo, klikoni që të vazhdohet. Këto janë veçori eksperimentale që mund të ngecin në rrugë të papritura. Përdorini me kujdes. @@ -918,8 +918,8 @@ Që të vazhdohet, ju lutemi, jepni fjalëkalimin tuaj. Ju lutemi, krijoni një frazëkalim për fshehtëzimin e kyçeve të eksportuar. Që të jeni në gjendje t’i importoni kyçet, do t’ju duhet të jepni të njëjtin frazëkalim. Kyçet E2E të dhomës u ruajtën te \'%s\'. - -Kujdes: kjo kartelë mund të fshihet, nëse çinstalohet aplikacioni. +\n +\nKujdes: kjo kartelë mund të fshihet, nëse çinstalohet aplikacioni. Mos dërgo kurrë mesazhe të fshehtëzuar, nga kjo pajisje te pajisje të paverifikuara. @@ -947,28 +947,28 @@ Kujdes: kjo kartelë mund të fshihet, nëse çinstalohet aplikacioni. Përgjegjësi i bashkësisë nuk ka dhënë një përshkrim të gjatë për këtë bashkësi. Kjo do ta bëjë llogarinë tuaj përgjithmonë të papërdorshme. S’do të jeni në gjendje të hyni në llogarinë tuaj, dhe askush s’do të jetë në gjendje të riregjistrojë të njëjtën ID përdoruesi. Kjo do të shkaktojë daljen e llogarisë tuaj nga krejt dhomat ku merr pjesë, dhe do të heqë hollësitë e llogarisë tuaj nga shërbyesi juaj i identiteteve. Ky veprim është i paprapakthyeshëm. - -Çaktivizimi i llogarisë tuaj nuk shkakton, si parazgjedhje, harrimin nga ne të mesazheve që keni dërguar. Nëse do të donit të harrojmë mesazhet tuaja, ju lutemi, i vini shenjë kutizës më poshtëplease tick the box below. - -Dukshmëria e mesazheve në Matrix është e ngjashme me atë në email. Harrimi i mesazheve nga ana jonë do të thotë që mesazhet që keni dërguar nuk do të ndahen me çfarëdo përdoruesi të ri apo të paregjistruar, por përdoruesit e regjistruar, që kanë tashmë hyrje në këto mesazhe, do të kenë prapëseprapë hyrje te kopja e tyre. - Të lutem, harro krejt mesazhet që kamë dërguar, kur të çaktivizohet llogaria ime (Kujdes: kjo do të bëjë që përdorues të ardhshëm të shohin një pamje jo të plotë të bisedave) +\n +\nÇaktivizimi i llogarisë tuaj nuk shkakton, si parazgjedhje, harrimin nga ne të mesazheve që keni dërguar. Nëse do të donit të harrojmë mesazhet tuaja, ju lutemi, i vini shenjë kutizës më poshtë. +\n +\nDukshmëria e mesazheve në Matrix është e ngjashme me atë në email. Harrimi i mesazheve nga ana jonë do të thotë që mesazhet që keni dërguar nuk do të ndahen me çfarëdo përdoruesi të ri apo të paregjistruar, por përdoruesit e regjistruar, që kanë tashmë hyrje në këto mesazhe, do të kenë prapëseprapë hyrje te kopja e tyre. + Të lutem, harro krejt mesazhet që kam dërguar, kur të çaktivizohet llogaria ime (Kujdes: kjo do të bëjë që përdorues të ardhshëm të shohin një pamje jo të plotë të bisedave) Ky shërbyes home ka tejkaluar një nga kufijtë mbi burimet, ndaj disa përdorues s’do të jenë në gjendje të bëjnë hyrjen. Ky shërbyes home ka tejkaluar kufirin Përdorues Aktivë Mujorë, ndaj disa përdorues s’do të jenë në gjendje të bëjnë hyrjen. Që të mund të diagnostikohen probleme, regjistra prej këtij klienti do të dërgohen tok me këtë njoftim të metash. Ky njoftim të metash, përfshi regjistrat dhe foton e ekranit, nuk do të jenë të dukshëm publikisht. Nëse do të parapëlqenit të dërgohej vetëm teksti më sipër, ju lutemi, hiqjani shenjën kutizës: - Që të prodhohen kyçe fshehtëzimi skaj-më-skaj për këtë pajisje, lypset të ribëni hyrjen dhe të parashtroni kyçin publik te shërbyesi juaj homë. -Kjo duhet vetëm një herë. -Na ndjeni për belanë. + Që të prodhohen kyçe fshehtëzimi skaj-më-skaj për këtë pajisje, lypset të ribëni hyrjen dhe të parashtroni kyçin publik te shërbyesi juaj Home. +\njo duhet vetëm një herë. +\nNa ndjeni për belanë. Kjo mund të ishte shenjë se dikush po përgjon me dashakeqësi trafikun tuaj, ose se telefoni juaj nuk i beson dëshmisë së furnizuar nga shërbyesi i largët. Nëse përgjegjësi i shërbyesit ka thënë se kjo është e pritshme, sigurohuni që shenjat e gishtave më poshtë përputhen me shenjat e gishtave të furnizuara prej tyre. Dëshmia ka ndryshuar nga ajo që qe besuar nga telefoni juaj. Kjo është SHUMË E PAZAKONTË. Këshillohet që TË MOS E PRANONI këtë dëshmi të re. Dëshmia ka ndryshuar nga një e besueshme dikur në një që nuk besohet. Shërbyesi mund të ketë rinovuar dëshminë e tij. Lidhuni me përgjegjësin e shërbyesit për shenjat e pritshme të gishtave. - Pranojeni dëshminë vetëm nëse përgjegjësi i i shërbyesit ka publikuar shenja gishtash që përputhen me ato më sipër. + Pranojeni dëshminë vetëm nëse përgjegjësi i shërbyesit ka publikuar shenja gishtash që përputhen me ato më sipër. Shfaq te rregullimet e sistemit të dhëna të aplikacionit. Aplikacioni lyp leje për xhirim në prapaskenë - • Njoftimet dërgohen përmes Google Cloud Messaging + • Njoftimet dërgohen përmes Firebase Cloud Messaging • Njoftimet përmbajnë vetëm tejtëdhëna • Lënda e mesazhit të njoftimit gjendet e siguruar drejt e nga shërbyesi home Matrix • Njoftimet përmbajnën tejtëdhëna dhe të dhëna mesazhi @@ -980,9 +980,9 @@ Na ndjeni për belanë. Riot-i mund të xhirojë në prapaskenë që të administrojë njoftimet tuaja në rrugë të sigurt dhe privatisht. Kjo mund të ndikojë në harxhimin e baterisë. Riot-i grumbullon të dhëna analitike anonime që të na lejojë ta përmirësojmë aplikacionin. Ju lutemi, aktivizoni analizat që të na ndihmoni të përmirësojmë Riot-in. - Të shfaqen krejt mesazhet prej %s? - -Kini parasysh që ky veprim do të sjellë rinisjen e aplikacionit dhe mund të hajë ca kohë. + Të shfaqen krejt mesazhet prej %s\? +\n +\nKini parasysh që ky veprim do të sjellë rinisjen e aplikacionit dhe mund të hajë ca kohë. Gabim gjatë vleftësimit të numrit tuaj të telefonit Nis kamerën e sistemit, në vend se skenën e kamerës vetjake. @@ -1035,42 +1035,42 @@ Kini parasysh që ky veprim do të sjellë rinisjen e aplikacionit dhe mund të Rregullime Sistemi. Njoftimet janë të aktivizuara te rregullimet e sistemit. Njoftimet janë të çaktivizuara te rregullimet e sistemit. -Ju lutemi, kontrolloni rregullimet e sistemit. +\nJu lutemi, kontrolloni rregullimet e sistemit. Hap Rregullimet Rregullime Llogarie. Njoftimet janë të aktivizuara për llogarinë tuaj. Njoftimet janë të çaktivizuara për llogarinë tuaj. -Ju lutemi, kontrolloni rregullime llogarie. +\nJu lutemi, kontrolloni rregullime llogarie. Aktivizoje Rregullime Pajisjeje. Njoftimet janë të aktivizuara për këtë pajisje. Nuk lejohen njoftime për këtë pajisje. -Ju lutemi, kontrolloni rregullimet e Riot-it. +\nJu lutemi, kontrolloni rregullimet e Riot-it. Aktivizoje Kontroll pë Play Services APK-ja për Google Play Services është e pranishme dhe e përditësuar. Riot-i përdor Google Play Services për të dorëzuar mesazhe push, por s’duket të jetë formësuar saktë: -%1$s +\n%1$s Ndreqni Play Services Token Firebase U mor me sukses token FCM: -%1$s +\n%1$s S’u arrit të merreh token FCM: -%1$s +\n%1$s Regjistrim Token-i Token-i FCM u regjistrua me sukses te Shërbyes Home. S’u arrit të regjistrohej token FCM te Shërbyes Home: -%1$s +\n%1$s Shërbim Njoftimesh Shërbimi i Njoftimeve po xhiron. Shërbimi i Njoftimeve s’po xhiron. -Provoni të rinisni aplikacionin. +\nProvoni të rinisni aplikacionin. Nise Shërbimin Vetërinisje Shërbimi Njoftimesh @@ -1079,15 +1079,15 @@ Provoni të rinisni aplikacionin. Nise gjatë nisjes së sistemit Shërbimi do të niset kur të riniset pajisja. - Shërbimi s’do të niset kur të riniset pajisja,s’do të merrni njoftime derisa Riot-i të jetë hapur një herë. + Shërbimi s’do të niset kur të riniset pajisja, s’do të merrni njoftime derisa Riot-i të jetë hapur një herë. Aktivizo Nisje gjatë nisjes së sistemit Kontrollo kufizime prapaskene Kufizimet për në prapaskenë janë të çaktivizuar për Riot-in. Ky test duhet të xhirojë duke përdorur të dhëna rrjeti celular (jo WIFI). -%1$s +\n%1$s Kufizimet për në prapaskenë janë të aktivizuara për Riot-in. -Puna që aplikacioni rreket të bëjë do të kufizohet në mënyrë agresive, teksa gjendet në prapaskenë, dhe kjo mund të prekë njoftimet. -%1$s +\nPuna që aplikacioni rreket të bëjë do të kufizohet në mënyrë agresive, teksa gjendet në prapaskenë, dhe kjo mund të prekë njoftimet. +\n%1$s Çaktivizoji kufizimet Optimizim Baterie @@ -1097,7 +1097,7 @@ Puna që aplikacioni rreket të bëjë do të kufizohet në mënyrë agresive, t Lidhje Në Prapaskenë Për t’ju dhënë njoftime të qëndrueshme, Riot-i lyp të mbajë në prapaskenë një lidhje me pak ndikim. -Në skenën pasuese do t’ju kërkohet të lejoni Riot-in të xhirojë në prapaskenë, ju lutemi, pranojeni. +\nNë skenën pasuese do t’ju kërkohet të lejoni Riot-in të xhirojë në prapaskenë, ju lutemi, pranojeni. Akordojini leje Ndodhi një gabim teksa verifikohej adresa juaj email. @@ -1107,38 +1107,23 @@ Në skenën pasuese do t’ju kërkohet të lejoni Riot-in të xhirojë në prap S’u gjet APK për Google Play Services. Njoftimet mund të mos punojnë saktë. - Riot.im - bashkëpunim ekipi të hapur - Mirë se vini te Riot.im: një botë e re komunikimesh të hapura! - Riot.im është një mjedis i thjeshtë dhe elegant bashkëpunimi, që mbledh tok në një aplikacion të vetëm krejt bisedat dhe integrime aplikacionesh tuajat të ndryshme. - -I ndërtuar rreth modelit të dhomave të bisedave, Riot.im ju lejon të shkëmbeni mesazhe, figura, video dhe kartela - të ndërveproni me mjetet tuaja dhe të merrni pjesë në krejt bashkësitë e ndryshme tuajat, që nga një vend i vetëm. Një identitet dhe një vend i vetëm për krejt ekipet ku merrni pjesë: s’ka nevojë të ndërrohen llogari, punoni dhe bisedoni me persona nga ente të ndryshme, publikisht ose në dhoma private: nga projekte profesionale e deri te ekskursionet shkollore, Riot.im do të shndërrohet në qendrën e krejt diskutimeve tuaja! - -Tanimë me fshehtëzim skaj-më-skaj! - -Në veçoritë përfshihen: - -• Ndani me të tjerët aty për aty mesazhe, figura, video dhe kartela të çfarëdo lloji, brenda grupesh të çfarëdo madhësie -• Zë dhe video tek për tek dhe thirrje në stil konference, përmes WebRTC-së -• Fshehtëzim skaj-më-skaj duke përdorur Olm-in (https://matrix.org/git/olm) -• Shihni kush po lexon mesazhet tuaj, përmes dëftesash leximi -• Komunikoni me përdorues kudoqoftë në ekosistemin Matrix.org - jo thjesht me përdorues të Riot.im-it! -• Zbuloni dhe ftoni përdorues përmes adresash email -• Merrni pjesë në dhoma publike të hapura ndaj vizitorësh -• Tejet i përshkallëzueshëm - mbulon qindra dhoma dhe mijëra përdorues -• Historik mesazhesh plotësisht i njëkohësuar në pajisje dhe shfletues të shumtë -• Rregullime të formësueshme shumë imët mbi njoftimet, të njëkohësuara përmes krejt pajisjeve -• Historik bisede i kërkueshëm pambarimisht -• Permalidhje për te mesazhe -• Kërkim i plotë mesazhesh -• Mbulim për gjuhë DNM, bie fjala, Arabishtja - -Për zhvilluesit: -• Riot.im është një klient Matrix - i ngritur mbi standardin dhe ekosistemin e hapur të Matrix.org, që furnizon ndërveprueshmëri me krejt aplikacionet, shërbimet dhe integrimet e tjera Matrix -• Tërësisht me burim të hapur nën licencën tolerante Apache License - merreni kodin prej https://github.com/vector-im/vector-android. Pull request-et janë të mirëpritur! -• I zgjerueshëm shumë thjesht, përmes API-t me burim të hapur Matrix Client-Server (https://matrix.org/docs/spec) -• Xhironi shërbyesin tuaj! Mund të përdorni shërbyesin parazgjedhje matrix.org ose të xhironi shërbyesin tuaj Matrix (p.sh. https://matrix.org/docs/projects/server/synapse.html) - -Zbuloni bashkëpunimin vërtet efikas dhe të hapur, me Riot.im! + Riot.im - Komunikoni, sipas mënyrës tuaj + Një aplikacion universal i sigurt bisedash, tërësisht nën kontrollin tuaj. + Një aplikacion fjalosjesh, nën kontrollin tuaj dhe plotësisht i zhdërvjellët. Riot-i ju lejon të komunikoni sipas mënyrës që doni. I krijuar për [matrix] - standardi për komunikime të hapura, të decentralizuara. +\n +\nMerrni një llogari matrix.org falas, merrni shërbyesin tuaj te https://modular.im, ose përdorni një tjetër shërbyes Matrix. +\n +\nPse të zgjidhet Riot.im\? +\n +\n• KOMUNIKIM I PLOTË: Krijoni dhoma rreth ekipeve tuaj, shokëve tuaj, bashkësisë tuaj - ç’të doni! Llafosuni, shkëmbeni kartela, shtoni widget-e dhe bëni thirrje me zë dhe figurë - gjithçka falas. +\n +\n• INTEGRIME TË FUQISHME: Përdoreni Riot.im me mjete që njihni dhe që i doni. Me Riot.im mundeni madje edhe të bisedoni me përdorues dhe grupe nën aplikacione të tjera fjalosjesh. +\n +\n• PRIVAT DHE I SIGURT: Mbajini bisedat tuaja të fshehta. Fshehtëzimi skaj-më-skaj i fjalës së fundit garanton që komunikimet private të mbeten private. +\n +\n• I HAPUR, JO I MBYLLUR: Me burim të hapur, i ngritur mbi Matrix. Jini zot i të dhënave tuaja, përmes strehimit të shërbyesit tuaj, ose duke përzgjedhur një të cilit i zini besë. +\n +\n• KUDO KU TË JENI: Mbani lidhjet kudo ku të jeni, me historik mesazhesh plotësisht të njëkohësuar nëpër krejt pajisjet tuaja dhe online te https://riot.im. Thirrje Video Në Kryerje e Sipër… @@ -1163,11 +1148,11 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani Kontrolloni Rregullimet [%1$s] -Ky gabim është jashtë kontrollit të Riot-it dhe, sipas Google-it, ky gabim është shenjë se pajisja ka shumë aplikacione të regjistruar me FCM. Gabimi ndodh vetëm në raste kur ka një numër të skajshëm aplikacionesh, ndaj nuk duhet të prekë përdoruesin mesatar. +\nKy gabim është jashtë kontrollit të Riot-it dhe, sipas Google-it, ky gabim është shenjë se pajisja ka shumë aplikacione të regjistruar me FCM. Gabimi ndodh vetëm në raste kur ka një numër të skajshëm aplikacionesh, ndaj nuk duhet të prekë përdoruesin mesatar. [%1$s] -Ky gabim është jashtë kontrollit të Riot-it. Mund të ndodhë për disa arsye. Ndoshta do të funksionojë, nëse riprovoni më vonë, mund të kontrolloni edhe nëse për Google Play Service s’ka kufizime lidhur me përdorimin e të dhënave, te rregullimet e sistemit, ose se ora e pajisjes suaj është e saktë, ose mund të ndodhë në ROM të përshtatur. +\nKy gabim është jashtë kontrollit të Riot-it. Mund të ndodhë për disa arsye. Ndoshta do të funksionojë, nëse riprovoni më vonë, mund të kontrolloni edhe nëse për Google Play Service s’ka kufizime lidhur me përdorimin e të dhënave, te rregullimet e sistemit, ose se ora e pajisjes suaj është e saktë, ose mund të ndodhë në ROM të përshtatur. [%1$s] -Ky gabim është jashtë kontrollit të Riot-it. S’ka llogari Google te telefoni. Ju lutemi, hapni përgjegjësin e llogarive dhe shtoni një llogari Google. +\nKy gabim është jashtë kontrollit të Riot-it. S’ka llogari Google te telefoni. Ju lutemi, hapni përgjegjësin e llogarive dhe shtoni një llogari Google. Shtoni Llogari Formësoni Njoftime të Zhurmshme @@ -1179,7 +1164,7 @@ Ky gabim është jashtë kontrollit të Riot-it. S’ka llogari Google te telefo Administrim Kyçesh Kriptografie Administroni Kopjeruajtje Kyçesh - I heshtur + Të heshtur Ju lutemi, jepni një frazëkalim Frazëkalimi është shumë i dobët @@ -1188,15 +1173,15 @@ Ky gabim është jashtë kontrollit të Riot-it. S’ka llogari Google te telefo Mos humbni kurrë mesazhe të fshehtëzuar Mesazhet në dhoma të fshehtëzuara sigurohen me fshehtëzim skaj-më-skaj. Vetëm ju dhe marrësi(t) keni kyçet për leximin e këtyre mesazheve. - -Bëni një kopjeruajtje të sigurt të kyçeve tuaj, për të shmangur humbjen e tyre. +\n +\nBëni një kopjeruajtje të sigurt të kyçeve tuaj, për të shmangur humbjen e tyre. Caktoni Frazëkalim U bë Ruani Kyç Rikthimesh Ruaje si Skedë Kyçi i rikthimeve u ruajt te \'%s\'. - -Kujdes: kjo kartelë mund të fshihet, nëse çinstalohet aplikacioni. +\n +\nKujdes: kjo kartelë mund të fshihet, nëse çinstalohet aplikacioni. Ju lutemi, bëni një kopje Jepjani kyçin e rikthimeve… @@ -1213,7 +1198,7 @@ Kujdes: kjo kartelë mund të fshihet, nëse çinstalohet aplikacioni. Po sillet version kopjeruajtjeje… Që të shkyçni historikun e mesazheve tuaj të sigurt, përdorni frazëkalimin tuaj të rikthimeve përdorni kyçin tuaj të rikthimeve - S’e dini frazëkalimin tuaj të rikthimeve? Mundeni të %s. + S’dihet frazëkalimi juaj i rikthimeve, mundeni të %s. Përdorni Kyçin tuaj të Rikthimeve për të shkyçur historikun tuaj të mesazheve të sigurt Jepni Kyç Rikthimesh @@ -1224,7 +1209,7 @@ Kujdes: kjo kartelë mund të fshihet, nëse çinstalohet aplikacioni. S’u shfshehtëzua dot kopjeruajtja me këtë frazëkalim: ju lutemi, verifikoni që dhatë frazëkalimin e duhur të rikthimeve. Gabim lidhjeje: ju lutemi, kontrolloni lidhjen tuaj dhe riprovoni. - Po rikthehet kopjeruajtja… + Po rikthehet kopjeruajtja: Shkyçeni Historikun Ju lutemi, jepni një kyç rikthimesh S’u shfshehtëzua dot kopjeruajtja me këtë kyç: ju lutemi, verifikoni që dhatë kyçin e duhur të rikthimeve. @@ -1290,16 +1275,15 @@ Kujdes: kjo kartelë mund të fshihet, nëse çinstalohet aplikacioni. Sigurojeni kopjeruajtjen tuaj me një Frazëkalim. Do të depozitojmë një kopje të fshehtëzuar të kyçeve tuaj në shërbyesin tonë. Mbrojeni kopjeruajtjen tuaj me një frazëkalim, për ta mbajtur të parrezikuar. - -Për maksimumin e sigurisë, ky duhet të jetë i ndryshëm nga fjalëkalimi juaj për llogarinë. +\n +\nPër maksimumin e sigurisë, ky duhet të jetë i ndryshëm nga fjalëkalimi juaj për llogarinë. Po Krijohet Kopjeruajtje Ose, sigurojeni kopjeruajtjen tuaj me një Kyç Rikthimesh, duke e ruajtur këtë diku të parrezikuar. (Të mëtejshme) Rregullojeni me një Kyç Rikthimesh Sukses! Kyçet tuaj po kopjeruhen. Kyçi juaj i rikthimeve është një lloj rrjeti sigurie - mund ta përdorni për të rifituar hyrje te mesazhet tuaj të fshehtëzuar, nëse harroni frazëkalimin tuaj. - -Mbajeni kyçin tuaj të rikthimeve diku shumë të sigurt, bie fjala, nën një përgjegjës fjalëkalimesh (ose në një kasafortë). +\nMbajeni kyçin tuaj të rikthimeve diku shumë të sigurt, bie fjala, nën një përgjegjës fjalëkalimesh (ose në një kasafortë) Mbajeni kyçin tuaj të rikthimeve diku në një vend shumë të sigurt, bie fjala, nën një përgjegjës fjalëkalimesh (ose në një kasafortë) Bëra një kopje Ndajeni me të tjerë @@ -1359,4 +1343,22 @@ Mbajeni kyçin tuaj të rikthimeve diku shumë të sigurt, bie fjala, nën një Zgjidhni Luaj tingull shkrepjeje + Vëri shenjë si të lexuar + Aplikacioni nuk ka nevojë të lidhet në prapaskenë me shërbyesin Home, kjo do të ulte harxhimin e baterisë + + %1$s: 1 mesazh + %1$s: %2$d mesazhe + + + %d njoftim + %d njoftime + + + Veprimtari e Re + Dhomë + Mesazhe të Rinj + Ftesë e Re + Unë + ** S’u arrit të dërgohej - ju lutemi, hapni dhomë + diff --git a/vector/src/main/res/values-tr/strings.xml b/vector/src/main/res/values-tr/strings.xml index c0927b86..31311214 100644 --- a/vector/src/main/res/values-tr/strings.xml +++ b/vector/src/main/res/values-tr/strings.xml @@ -1180,7 +1180,7 @@ Ayrıca e-posta adresi, şifrenizi sıfırlamanıza da olanak tanır. %d+ Geçerli Google Play Hizmetleri APK\'sı bulunamadı. Bildirimler olması gerektiği gibi çalışmayacak. - Riot.im - İletişim, senin yolunda. + Riot.im - İletişim, senin yolunda "Biz her zaman Riot.im’e geliştirmeler ve değişimler yapıyoruz. Tam değişiklikler listesi burada bulunabilir: %1$s. Bir şeyleri kaçırmamak için güncellemeleri açık tutun." diff --git a/vector/src/main/res/values-zh-rCN/strings.xml b/vector/src/main/res/values-zh-rCN/strings.xml index 42979c88..9703af75 100644 --- a/vector/src/main/res/values-zh-rCN/strings.xml +++ b/vector/src/main/res/values-zh-rCN/strings.xml @@ -838,7 +838,7 @@ 获取权限 选择其他选项 - • 通知通过 Google Cloud Messaging 发送 + • 通知通过 Firebase Cloud Messaging 发送 • 通知只含有元数据 • 通知不会显示消息内容 @@ -1092,7 +1092,7 @@ Matrix 中的消息可见性类似于电子邮件。我们忘记您的消息意 +%d %1$s:%2$s %1$s: - Riot.im - 沟通,由你掌控。 + Riot.im - 沟通,由你掌控 一款完全由你掌控的通用安全聊天应用。 总是 消息与错误 @@ -1355,4 +1355,28 @@ Riot 在后台时的工作将被显著的限制,这可能会影响消息通知 \n%s 使用设置 + 正在初始化服务 + 媒体 + 默认压缩 + 选择 + 默认媒体来源 + 选择 + 播放快门声 + + 标记为已读 + 本应用 需要在后台连接主服务器,应能减少电量消耗 + + %1$s: %2$d 条消息 + + + %d 条通知 + + + 新活动 + 聊天室 + 新消息 + 新邀请 + + ** 发送失败 - 请打开聊天室 + diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index 1bfc12cb..66d5f220 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -499,7 +499,7 @@ 標準 低隱私模式 應用程式需要權限以在後臺運行 - •通知將通過 GCM(Google Cloud Messaging) 發送 + •通知將通過 Firebase Cloud Messaging 發送 ·通知僅包含中繼資料 • 通知的消息內容將從 Matrix 主服務器安全獲取 • 通知含有訊息與中繼資料 @@ -1106,7 +1106,7 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 找不到有效的 Google Play 服務 APK。通知可能無法正常運作。 - Riot.im - 以您的方式溝通。 + Riot.im - 以您的方式溝通 一個完全由您控制的安全聊天應用程式。 "完全由您控制且極具彈性的聊天應用程式。Riot 讓您以您想要的方式溝通。為 [matrix] 而生,其為開放、去中心化的通訊標準。 @@ -1339,4 +1339,20 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 選擇 播放快門聲 + 標示為已讀 + 應用程式需要在背景連線到家伺服器,它應該可以降低耗電量 + + %1$s:%2$d 則訊息 + + + %d 個通知 + + + 新活動 + 聊天室 + 新訊息 + 新邀請 + + ** 傳送失敗 - 請開啟聊天室 + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index c907f593..5758dcf3 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -14,7 +14,7 @@ Initializing service - Synchronising + Synchronising… Listening for events Noisy notifications Silent notifications @@ -106,6 +106,7 @@ Mark all as read Historical Quick reply + Mark as read Open Close Copied to clipboard @@ -1075,6 +1076,8 @@ %d active widgets + Sorry, conference calls with Jitsi are not supported on old devices (devices with Android OS below 5.0) + Unable to create widget. Failed to send request. @@ -1234,7 +1237,7 @@ No valid Google Play Services APK found. Notifications may not work properly. - Riot.im - Communicate, your way. + Riot.im - Communicate, your way "We’re always making changes and improvements to Riot.im. The complete changelog can be found here: %1$s. To make sure you don’t miss a thing, just keep your updates turned on." From b9b8527b38d4d7dd795fae85aa1d992f6f83b791 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 2 Apr 2019 12:14:16 +0200 Subject: [PATCH 02/12] Improve RiotFragment --- .../core/platform/RiotFragment.kt | 68 +++++++++++++++++++ .../core/platform/SimpleTextWatcher.kt | 37 ++++++++++ .../features/home/HomeDrawerFragment.kt | 7 +- .../features/home/group/GroupListFragment.kt | 7 +- .../room/detail/LoadingRoomDetailFragment.kt | 6 +- .../home/room/detail/RoomDetailFragment.kt | 6 +- .../home/room/list/RoomListFragment.kt | 7 +- 7 files changed, 110 insertions(+), 28 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotredesign/core/platform/SimpleTextWatcher.kt diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt index 0b7996fa..076d88b2 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt @@ -18,17 +18,70 @@ package im.vector.riotredesign.core.platform import android.os.Bundle import android.os.Parcelable +import android.view.* +import androidx.annotation.CallSuper +import androidx.annotation.LayoutRes import androidx.annotation.MainThread +import butterknife.ButterKnife +import butterknife.Unbinder import com.airbnb.mvrx.BaseMvRxFragment import com.airbnb.mvrx.MvRx import com.bumptech.glide.util.Util.assertMainThread +import timber.log.Timber abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed { + // Butterknife unbinder + private var mUnBinder: Unbinder? = null + val riotActivity: RiotActivity by lazy { activity as RiotActivity } + /* ========================================================================================== + * Life cycle + * ========================================================================================== */ + + @CallSuper + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (getMenuRes() != -1) { + setHasOptionsMenu(true) + } + } + + final override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(getLayoutResId(), container, false) + } + + @LayoutRes + abstract fun getLayoutResId(): Int + + @CallSuper + override fun onResume() { + super.onResume() + + Timber.d("onResume Fragment ${this.javaClass.simpleName}") + } + + @CallSuper + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + mUnBinder = ButterKnife.bind(this, view) + } + + @CallSuper + override fun onDestroyView() { + super.onDestroyView() + mUnBinder?.unbind() + mUnBinder = null + } + + /* ========================================================================================== + * Restorable + * ========================================================================================== */ + private val restorables = ArrayList() override fun onSaveInstanceState(outState: Bundle) { @@ -60,4 +113,19 @@ abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed { return this } + + /* ========================================================================================== + * MENU MANAGEMENT + * ========================================================================================== */ + + open fun getMenuRes() = -1 + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + val menuRes = getMenuRes() + + if (menuRes != -1) { + inflater.inflate(menuRes, menu) + } + } + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/SimpleTextWatcher.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/SimpleTextWatcher.kt new file mode 100644 index 00000000..d2229542 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/SimpleTextWatcher.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.platform + +import android.text.Editable +import android.text.TextWatcher + +/** + * TextWatcher with default no op implementation + */ +open class SimpleTextWatcher : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + // No op + } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + // No op + } + + override fun afterTextChanged(s: Editable) { + // No op + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt index 981f786b..dafdc990 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt @@ -17,9 +17,6 @@ package im.vector.riotredesign.features.home import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup import im.vector.riotredesign.R import im.vector.riotredesign.core.extensions.replaceChildFragment import im.vector.riotredesign.core.platform.RiotFragment @@ -35,9 +32,7 @@ class HomeDrawerFragment : RiotFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_home_drawer, container, false) - } + override fun getLayoutResId() = R.layout.fragment_home_drawer override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt index fb99e839..f8290688 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt @@ -17,9 +17,6 @@ package im.vector.riotredesign.features.home.group import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel @@ -44,9 +41,7 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback { private val viewModel: GroupListViewModel by fragmentViewModel() private val groupController by inject() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_group_list, container, false) - } + override fun getLayoutResId() = R.layout.fragment_group_list override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/LoadingRoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/LoadingRoomDetailFragment.kt index 16f2e9a1..6eb4c35a 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/LoadingRoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/LoadingRoomDetailFragment.kt @@ -17,9 +17,7 @@ package im.vector.riotredesign.features.home.room.detail import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import com.bumptech.glide.Glide import im.vector.riotredesign.R import im.vector.riotredesign.core.platform.RiotFragment @@ -34,9 +32,7 @@ class LoadingRoomDetailFragment : RiotFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_loading_room_detail, container, false) - } + override fun getLayoutResId() = R.layout.fragment_loading_room_detail override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 30a262c4..a66362ec 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -18,9 +18,7 @@ package im.vector.riotredesign.features.home.room.detail import android.os.Bundle import android.os.Parcelable -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyVisibilityTracker @@ -69,9 +67,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_room_detail, container, false) - } + override fun getLayoutResId() = R.layout.fragment_room_detail override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt index a1e98068..ece0da61 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt @@ -19,9 +19,6 @@ package im.vector.riotredesign.features.home.room.list import android.os.Bundle import android.text.Editable import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete @@ -54,9 +51,7 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback { private val homeNavigator by inject() private val roomListViewModel: RoomListViewModel by fragmentViewModel() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_room_list, container, false) - } + override fun getLayoutResId() = R.layout.fragment_room_list override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) From 6830957d319297fda9ca719be483b27935780b94 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 2 Apr 2019 18:08:43 +0200 Subject: [PATCH 03/12] Import settings from Riot - not all fonctional of course --- build.gradle | 1 + .../matrix/android/api/session/Session.kt | 4 +- .../api/session/crypto/CryptoService.kt | 23 + vector/build.gradle | 16 +- vector/src/fdroid/AndroidManifest.xml | 15 + .../java/im/vector/push/fcm/FcmHelper.java | 56 + ...ificationTroubleshootTestManagerFactory.kt | 50 + .../fcm/troubleshoot/TestAutoStartBoot.kt | 44 + .../TestBackgroundRestrictions.kt | 71 + .../troubleshoot/TestBatteryOptimization.kt | 47 + .../OnApplicationUpgradeReceiver.java | 34 + vector/src/gplay/AndroidManifest.xml | 19 + vector/src/gplay/google-services.json | 46 + .../java/im/vector/push/fcm/FcmHelper.java | 120 + ...ificationTroubleshootTestManagerFactory.kt | 49 + .../fcm/VectorFirebaseMessagingService.kt | 271 ++ .../fcm/troubleshoot/TestFirebaseToken.kt | 74 + .../push/fcm/troubleshoot/TestPlayServices.kt | 55 + .../fcm/troubleshoot/TestTokenRegistration.kt | 57 + vector/src/main/AndroidManifest.xml | 9 + .../vector/riotredesign/core/di/AppModule.kt | 5 + .../core/extensions/UrlExtensions.kt | 37 + .../core/extensions/ViewExtensions.kt | 50 + .../core/preference/AddressPreference.kt | 69 + .../core/preference/BingRulePreference.kt | 243 ++ .../core/preference/ProgressBarPreference.kt | 35 + .../core/preference/RoomAvatarPreference.kt | 51 + .../core/preference/UserAvatarPreference.kt | 65 + .../preference/VectorEditTextPreference.kt | 54 + .../core/preference/VectorGroupPreference.kt | 105 + .../core/preference/VectorListPreference.kt | 96 + .../core/preference/VectorPreference.kt | 163 + .../preference/VectorPreferenceCategory.kt | 52 + .../preference/VectorPreferenceDivider.kt | 36 + .../core/preference/VectorSwitchPreference.kt | 49 + .../riotredesign/core/services/CallService.kt | 207 ++ .../core/services/EventStreamServiceX.kt | 583 ++++ .../core/services/PushSimulatorWorker.kt | 36 + .../core/services/VectorService.kt | 58 + .../riotredesign/core/utils/RingtoneUtils.kt | 119 + .../core/utils/SecretStoringUtils.kt | 576 ++++ .../riotredesign/core/utils/SystemUtils.kt | 10 +- .../riotredesign/features/badge/BadgeProxy.kt | 133 + .../features/home/HomeActivity.kt | 7 + .../homeserver/ServerUrlsRepository.kt | 107 + .../features/notifications/IconLoader.kt | 128 + .../notifications/InviteNotifiableEvent.kt | 36 + .../features/notifications/NotifiableEvent.kt | 36 + .../notifications/NotifiableEventResolver.kt | 188 ++ .../notifications/NotifiableMessageEvent.kt | 57 + .../NotificationBroadcastReceiver.kt | 182 + .../NotificationDrawerManager.kt | 463 +++ .../notifications/NotificationUtils.kt | 721 ++++ .../notifications/OutdatedEventDetector.kt | 48 + .../notifications/RoomEventGroupInfo.kt | 34 + .../notifications/SimpleNotifiableEvent.kt | 35 + .../features/settings/PreferencesManager.java | 861 +++++ .../settings/VectorSettingsActivity.kt | 116 + ...sAdvancedNotificationPreferenceFragment.kt | 296 ++ ...ctorSettingsFragmentInteractionListener.kt | 24 + ...ttingsNotificationsTroubleshootFragment.kt | 183 ++ .../VectorSettingsPreferencesFragment.kt | 2927 +++++++++++++++++ ...ficationTroubleshootRecyclerViewAdapter.kt | 127 + .../NotificationTroubleshootTestManager.kt | 101 + .../troubleshoot/TestAccountSettings.kt | 65 + .../troubleshoot/TestBingRulesSettings.kt | 85 + .../troubleshoot/TestDeviceSettings.kt | 47 + .../troubleshoot/TestSystemSettings.kt | 45 + .../settings/troubleshoot/TroubleshootTest.kt | 54 + .../main/res/anim/anim_slide_in_bottom.xml | 9 + .../src/main/res/anim/anim_slide_nothing.xml | 9 + .../main/res/anim/anim_slide_out_bottom.xml | 14 + .../src/main/res/anim/unread_marker_anim.xml | 16 + .../src/main/res/drawable-hdpi/unit_test.png | Bin 0 -> 684 bytes .../main/res/drawable-hdpi/unit_test_ko.png | Bin 0 -> 577 bytes .../main/res/drawable-hdpi/unit_test_ok.png | Bin 0 -> 838 bytes .../src/main/res/drawable-mdpi/unit_test.png | Bin 0 -> 411 bytes .../main/res/drawable-mdpi/unit_test_ko.png | Bin 0 -> 388 bytes .../main/res/drawable-mdpi/unit_test_ok.png | Bin 0 -> 548 bytes .../src/main/res/drawable-xhdpi/unit_test.png | Bin 0 -> 893 bytes .../main/res/drawable-xhdpi/unit_test_ko.png | Bin 0 -> 742 bytes .../main/res/drawable-xhdpi/unit_test_ok.png | Bin 0 -> 1119 bytes .../main/res/drawable-xxhdpi/ic_add_black.png | Bin 0 -> 114 bytes .../main/res/drawable-xxhdpi/ic_eye_black.png | Bin 0 -> 2750 bytes .../drawable-xxhdpi/ic_eye_closed_black.png | Bin 0 -> 3084 bytes .../ic_material_call_end_grey.png | Bin 0 -> 4424 bytes .../ic_material_done_all_white.png | Bin 0 -> 398 bytes .../ic_material_done_white.png | Bin 0 -> 255 bytes .../main/res/drawable-xxhdpi/ic_settings.png | Bin 0 -> 1510 bytes .../drawable-xxhdpi/icon_notif_important.png | Bin 0 -> 2121 bytes ...incoming_call_notification_transparent.png | Bin 0 -> 684 bytes .../res/drawable-xxhdpi/logo_transparent.png | Bin 0 -> 1666 bytes .../res/drawable-xxhdpi/main_alias_icon.png | Bin 0 -> 1792 bytes .../main/res/drawable-xxhdpi/unit_test.png | Bin 0 -> 1334 bytes .../main/res/drawable-xxhdpi/unit_test_ko.png | Bin 0 -> 1133 bytes .../main/res/drawable-xxhdpi/unit_test_ok.png | Bin 0 -> 1621 bytes .../vector_notification_accept_invitation.png | Bin 0 -> 473 bytes .../vector_notification_open.png | Bin 0 -> 318 bytes .../vector_notification_quick_reply.png | Bin 0 -> 269 bytes .../vector_notification_reject_invitation.png | Bin 0 -> 309 bytes .../drawable-xxhdpi/vector_warning_red.png | Bin 0 -> 436 bytes .../main/res/drawable-xxxhdpi/unit_test.png | Bin 0 -> 1792 bytes .../res/drawable-xxxhdpi/unit_test_ko.png | Bin 0 -> 1473 bytes .../res/drawable-xxxhdpi/unit_test_ok.png | Bin 0 -> 2190 bytes .../res/layout/activity_vector_settings.xml | 41 + .../main/res/layout/dialog_base_edit_text.xml | 21 + .../res/layout/dialog_change_password.xml | 87 + .../main/res/layout/dialog_device_details.xml | 66 + .../layout/dialog_preference_edit_text.xml | 27 + .../res/layout/dialog_select_text_size.xml | 85 + ...nt_settings_notifications_troubleshoot.xml | 91 + .../layout/item_notification_troubleshoot.xml | 94 + .../layout/vector_preference_bing_rule.xml | 81 + .../res/layout/vector_preference_divider.xml | 21 + .../vector_settings_address_preference.xml | 13 + ..._settings_list_preference_with_warning.xml | 13 + .../layout/vector_settings_round_avatar.xml | 24 + .../vector_settings_round_group_avatar.xml | 18 + .../vector_settings_spinner_preference.xml | 12 + vector/src/main/res/menu/home.xml | 11 + vector/src/main/res/values/array.xml | 136 + ...ings_notification_advanced_preferences.xml | 66 + .../res/xml/vector_settings_preferences.xml | 504 +++ 123 files changed, 12318 insertions(+), 7 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt create mode 100644 vector/src/fdroid/AndroidManifest.xml create mode 100755 vector/src/fdroid/java/im/vector/push/fcm/FcmHelper.java create mode 100644 vector/src/fdroid/java/im/vector/push/fcm/NotificationTroubleshootTestManagerFactory.kt create mode 100644 vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestAutoStartBoot.kt create mode 100644 vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestBackgroundRestrictions.kt create mode 100644 vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestBatteryOptimization.kt create mode 100644 vector/src/fdroid/java/im/vector/receiver/OnApplicationUpgradeReceiver.java create mode 100755 vector/src/gplay/AndroidManifest.xml create mode 100644 vector/src/gplay/google-services.json create mode 100755 vector/src/gplay/java/im/vector/push/fcm/FcmHelper.java create mode 100644 vector/src/gplay/java/im/vector/push/fcm/NotificationTroubleshootTestManagerFactory.kt create mode 100755 vector/src/gplay/java/im/vector/push/fcm/VectorFirebaseMessagingService.kt create mode 100644 vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestFirebaseToken.kt create mode 100644 vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestPlayServices.kt create mode 100644 vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestTokenRegistration.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/extensions/UrlExtensions.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/extensions/ViewExtensions.kt create mode 100755 vector/src/main/java/im/vector/riotredesign/core/preference/AddressPreference.kt create mode 100755 vector/src/main/java/im/vector/riotredesign/core/preference/BingRulePreference.kt create mode 100755 vector/src/main/java/im/vector/riotredesign/core/preference/ProgressBarPreference.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/preference/RoomAvatarPreference.kt create mode 100755 vector/src/main/java/im/vector/riotredesign/core/preference/UserAvatarPreference.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/preference/VectorEditTextPreference.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/preference/VectorGroupPreference.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/preference/VectorListPreference.kt create mode 100755 vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreference.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreferenceCategory.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreferenceDivider.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/preference/VectorSwitchPreference.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/services/CallService.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/services/EventStreamServiceX.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/services/PushSimulatorWorker.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/services/VectorService.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/utils/RingtoneUtils.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/utils/SecretStoringUtils.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/badge/BadgeProxy.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/homeserver/ServerUrlsRepository.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/notifications/IconLoader.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/notifications/InviteNotifiableEvent.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEvent.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEventResolver.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableMessageEvent.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt create mode 100755 vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationUtils.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/notifications/OutdatedEventDetector.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/notifications/RoomEventGroupInfo.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/notifications/SimpleNotifiableEvent.kt create mode 100755 vector/src/main/java/im/vector/riotredesign/features/settings/PreferencesManager.java create mode 100755 vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsFragmentInteractionListener.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt create mode 100755 vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/settings/troubleshoot/NotificationTroubleshootRecyclerViewAdapter.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/settings/troubleshoot/NotificationTroubleshootTestManager.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/settings/troubleshoot/TestAccountSettings.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/settings/troubleshoot/TestBingRulesSettings.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/settings/troubleshoot/TestDeviceSettings.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/settings/troubleshoot/TestSystemSettings.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/settings/troubleshoot/TroubleshootTest.kt create mode 100644 vector/src/main/res/anim/anim_slide_in_bottom.xml create mode 100644 vector/src/main/res/anim/anim_slide_nothing.xml create mode 100644 vector/src/main/res/anim/anim_slide_out_bottom.xml create mode 100644 vector/src/main/res/anim/unread_marker_anim.xml create mode 100644 vector/src/main/res/drawable-hdpi/unit_test.png create mode 100644 vector/src/main/res/drawable-hdpi/unit_test_ko.png create mode 100644 vector/src/main/res/drawable-hdpi/unit_test_ok.png create mode 100644 vector/src/main/res/drawable-mdpi/unit_test.png create mode 100644 vector/src/main/res/drawable-mdpi/unit_test_ko.png create mode 100644 vector/src/main/res/drawable-mdpi/unit_test_ok.png create mode 100644 vector/src/main/res/drawable-xhdpi/unit_test.png create mode 100644 vector/src/main/res/drawable-xhdpi/unit_test_ko.png create mode 100644 vector/src/main/res/drawable-xhdpi/unit_test_ok.png create mode 100755 vector/src/main/res/drawable-xxhdpi/ic_add_black.png create mode 100644 vector/src/main/res/drawable-xxhdpi/ic_eye_black.png create mode 100644 vector/src/main/res/drawable-xxhdpi/ic_eye_closed_black.png create mode 100644 vector/src/main/res/drawable-xxhdpi/ic_material_call_end_grey.png create mode 100755 vector/src/main/res/drawable-xxhdpi/ic_material_done_all_white.png create mode 100755 vector/src/main/res/drawable-xxhdpi/ic_material_done_white.png create mode 100755 vector/src/main/res/drawable-xxhdpi/ic_settings.png create mode 100644 vector/src/main/res/drawable-xxhdpi/icon_notif_important.png create mode 100755 vector/src/main/res/drawable-xxhdpi/incoming_call_notification_transparent.png create mode 100755 vector/src/main/res/drawable-xxhdpi/logo_transparent.png create mode 100644 vector/src/main/res/drawable-xxhdpi/main_alias_icon.png create mode 100644 vector/src/main/res/drawable-xxhdpi/unit_test.png create mode 100644 vector/src/main/res/drawable-xxhdpi/unit_test_ko.png create mode 100644 vector/src/main/res/drawable-xxhdpi/unit_test_ok.png create mode 100755 vector/src/main/res/drawable-xxhdpi/vector_notification_accept_invitation.png create mode 100644 vector/src/main/res/drawable-xxhdpi/vector_notification_open.png create mode 100755 vector/src/main/res/drawable-xxhdpi/vector_notification_quick_reply.png create mode 100755 vector/src/main/res/drawable-xxhdpi/vector_notification_reject_invitation.png create mode 100755 vector/src/main/res/drawable-xxhdpi/vector_warning_red.png create mode 100644 vector/src/main/res/drawable-xxxhdpi/unit_test.png create mode 100644 vector/src/main/res/drawable-xxxhdpi/unit_test_ko.png create mode 100644 vector/src/main/res/drawable-xxxhdpi/unit_test_ok.png create mode 100755 vector/src/main/res/layout/activity_vector_settings.xml create mode 100644 vector/src/main/res/layout/dialog_base_edit_text.xml create mode 100644 vector/src/main/res/layout/dialog_change_password.xml create mode 100644 vector/src/main/res/layout/dialog_device_details.xml create mode 100644 vector/src/main/res/layout/dialog_preference_edit_text.xml create mode 100644 vector/src/main/res/layout/dialog_select_text_size.xml create mode 100644 vector/src/main/res/layout/fragment_settings_notifications_troubleshoot.xml create mode 100644 vector/src/main/res/layout/item_notification_troubleshoot.xml create mode 100644 vector/src/main/res/layout/vector_preference_bing_rule.xml create mode 100644 vector/src/main/res/layout/vector_preference_divider.xml create mode 100644 vector/src/main/res/layout/vector_settings_address_preference.xml create mode 100644 vector/src/main/res/layout/vector_settings_list_preference_with_warning.xml create mode 100644 vector/src/main/res/layout/vector_settings_round_avatar.xml create mode 100644 vector/src/main/res/layout/vector_settings_round_group_avatar.xml create mode 100644 vector/src/main/res/layout/vector_settings_spinner_preference.xml create mode 100644 vector/src/main/res/menu/home.xml create mode 100644 vector/src/main/res/values/array.xml create mode 100644 vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml create mode 100755 vector/src/main/res/xml/vector_settings_preferences.xml diff --git a/build.gradle b/build.gradle index 7e567983..479eb154 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ buildscript { } } dependencies { classpath 'com.android.tools.build:gradle:3.3.2' + classpath 'com.google.gms:google-services:4.2.0' classpath "com.airbnb.okreplay:gradle-plugin:1.4.0" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.6.2' diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index ad453282..e5ee1c0e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session import androidx.annotation.MainThread import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.content.ContentUrlResolver +import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.user.UserService @@ -27,7 +28,7 @@ import im.vector.matrix.android.api.session.user.UserService * This interface defines interactions with a session. * An instance of a session will be provided by the SDK. */ -interface Session : RoomService, GroupService, UserService { +interface Session : RoomService, GroupService, UserService, CryptoService { /** * The params associated to the session @@ -69,5 +70,4 @@ interface Session : RoomService, GroupService, UserService { // Not used at the moment interface Listener - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt new file mode 100644 index 00000000..86c8a86f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.crypto + +interface CryptoService { + + // Not supported for the moment + fun isCryptoEnabled() = false +} \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index 6856d591..3957a8a0 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -74,10 +74,12 @@ android { buildTypes { debug { resValue "bool", "debug_mode", "true" + buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" } release { resValue "bool", "debug_mode", "false" + buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' @@ -181,6 +183,9 @@ dependencies { kapt "com.airbnb.android:epoxy-processor:$epoxy_version" implementation 'com.airbnb.android:mvrx:0.7.0' + // Work + implementation "android.arch.work:work-runtime-ktx:1.0.0" + // FP implementation "io.arrow-kt:arrow-core:$arrow_version" @@ -209,14 +214,23 @@ dependencies { implementation "com.github.bumptech.glide:glide:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version" + // Badge for compatibility + implementation 'me.leolin:ShortcutBadger:1.1.2@aar' + // DI implementation "org.koin:koin-android:$koin_version" implementation "org.koin:koin-android-scope:$koin_version" + // gplay flavor only + gplayImplementation 'com.google.firebase:firebase-core:16.0.8' + gplayImplementation 'com.google.firebase:firebase-messaging:17.5.0' + // TESTS testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } - +if (!getGradle().getStartParameter().getTaskRequests().toString().contains("fdroid")) { + apply plugin: 'com.google.gms.google-services' +} diff --git a/vector/src/fdroid/AndroidManifest.xml b/vector/src/fdroid/AndroidManifest.xml new file mode 100644 index 00000000..01babce9 --- /dev/null +++ b/vector/src/fdroid/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/fdroid/java/im/vector/push/fcm/FcmHelper.java b/vector/src/fdroid/java/im/vector/push/fcm/FcmHelper.java new file mode 100755 index 00000000..ba8badef --- /dev/null +++ b/vector/src/fdroid/java/im/vector/push/fcm/FcmHelper.java @@ -0,0 +1,56 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.push.fcm; + +import android.app.Activity; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class FcmHelper { + + /** + * Retrieves the FCM registration token. + * + * @return the FCM token or null if not received from FCM + */ + @Nullable + public static String getFcmToken(Context context) { + return null; + } + + /** + * Store FCM token to the SharedPrefs + * + * @param context android context + * @param token the token to store + */ + public static void storeFcmToken(@NonNull Context context, + @Nullable String token) { + // No op + } + + /** + * onNewToken may not be called on application upgrade, so ensure my shared pref is set + * + * @param activity the first launch Activity + */ + public static void ensureFcmTokenIsRetrieved(final Activity activity) { + // No op + } +} diff --git a/vector/src/fdroid/java/im/vector/push/fcm/NotificationTroubleshootTestManagerFactory.kt b/vector/src/fdroid/java/im/vector/push/fcm/NotificationTroubleshootTestManagerFactory.kt new file mode 100644 index 00000000..5a81c79d --- /dev/null +++ b/vector/src/fdroid/java/im/vector/push/fcm/NotificationTroubleshootTestManagerFactory.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.push.fcm + +import androidx.fragment.app.Fragment +import im.vector.fragments.troubleshoot.TestAccountSettings +import im.vector.fragments.troubleshoot.TestDeviceSettings +import im.vector.matrix.android.api.session.Session +import im.vector.push.fcm.troubleshoot.TestAutoStartBoot +import im.vector.push.fcm.troubleshoot.TestBackgroundRestrictions +import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager +import im.vector.riotredesign.features.settings.troubleshoot.TestBingRulesSettings +import im.vector.riotredesign.features.settings.troubleshoot.TestSystemSettings + +class NotificationTroubleshootTestManagerFactory { + + companion object { + fun createTestManager(fragment: Fragment, session: Session?): NotificationTroubleshootTestManager { + val mgr = NotificationTroubleshootTestManager(fragment) + mgr.addTest(TestSystemSettings(fragment)) + if (session != null) { + mgr.addTest(TestAccountSettings(fragment, session)) + } + mgr.addTest(TestDeviceSettings(fragment)) + if (session != null) { + mgr.addTest(TestBingRulesSettings(fragment, session)) + } + // mgr.addTest(TestNotificationServiceRunning(fragment)) + // mgr.addTest(TestServiceRestart(fragment)) + mgr.addTest(TestAutoStartBoot(fragment)) + mgr.addTest(TestBackgroundRestrictions(fragment)) + // mgr.addTest(TestBatteryOptimization(fragment)) + return mgr + } + } + +} \ No newline at end of file diff --git a/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestAutoStartBoot.kt b/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestAutoStartBoot.kt new file mode 100644 index 00000000..4e334236 --- /dev/null +++ b/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestAutoStartBoot.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.push.fcm.troubleshoot + +import androidx.fragment.app.Fragment +import im.vector.riotredesign.R +import im.vector.riotredesign.features.settings.PreferencesManager +import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest + +/** + * Test that the application is started on boot + */ +class TestAutoStartBoot(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_service_boot_title) { + + override fun perform() { + if (PreferencesManager.autoStartOnBoot(fragment.context)) { + description = fragment.getString(R.string.settings_troubleshoot_test_service_boot_success) + status = TestStatus.SUCCESS + quickFix = null + } else { + description = fragment.getString(R.string.settings_troubleshoot_test_service_boot_failed) + quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_service_boot_quickfix) { + override fun doFix() { + PreferencesManager.setAutoStartOnBoot(fragment.context, true) + manager?.retry() + } + } + status = TestStatus.FAILED + } + } +} \ No newline at end of file diff --git a/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestBackgroundRestrictions.kt b/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestBackgroundRestrictions.kt new file mode 100644 index 00000000..1ad298c6 --- /dev/null +++ b/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestBackgroundRestrictions.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.push.fcm.troubleshoot + +import android.content.Context +import android.net.ConnectivityManager +import androidx.core.net.ConnectivityManagerCompat +import androidx.fragment.app.Fragment +import im.vector.riotredesign.R +import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest + +class TestBackgroundRestrictions(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_bg_restricted_title) { + + override fun perform() { + (fragment.context!!.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).apply { + // Checks if the device is on a metered network + if (isActiveNetworkMetered) { + // Checks user’s Data Saver settings. + val restrictBackgroundStatus = ConnectivityManagerCompat.getRestrictBackgroundStatus(this) + when (restrictBackgroundStatus) { + ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED -> { + // Background data usage is blocked for this app. Wherever possible, + // the app should also use less data in the foreground. + description = fragment.getString(R.string.settings_troubleshoot_test_bg_restricted_failed, + "RESTRICT_BACKGROUND_STATUS_ENABLED") + status = TestStatus.FAILED + quickFix = null + } + ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED -> { + // The app is whitelisted. Wherever possible, + // the app should use less data in the foreground and background. + description = fragment.getString(R.string.settings_troubleshoot_test_bg_restricted_success, + "RESTRICT_BACKGROUND_STATUS_WHITELISTED") + status = TestStatus.SUCCESS + quickFix = null + } + ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED -> { + // Data Saver is disabled. Since the device is connected to a + // metered network, the app should use less data wherever possible. + description = fragment.getString(R.string.settings_troubleshoot_test_bg_restricted_success, + "RESTRICT_BACKGROUND_STATUS_DISABLED") + status = TestStatus.SUCCESS + quickFix = null + } + + } + + } else { + // The device is not on a metered network. + // Use data as required to perform syncs, downloads, and updates. + description = fragment.getString(R.string.settings_troubleshoot_test_bg_restricted_success, "") + status = TestStatus.SUCCESS + quickFix = null + } + } + } + +} \ No newline at end of file diff --git a/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestBatteryOptimization.kt b/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestBatteryOptimization.kt new file mode 100644 index 00000000..c7d60018 --- /dev/null +++ b/vector/src/fdroid/java/im/vector/push/fcm/troubleshoot/TestBatteryOptimization.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.push.fcm.troubleshoot + +import androidx.fragment.app.Fragment +import im.vector.riotredesign.R +import im.vector.riotredesign.core.utils.isIgnoringBatteryOptimizations +import im.vector.riotredesign.core.utils.requestDisablingBatteryOptimization +import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager +import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest + +// Not used anymore +class TestBatteryOptimization(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_battery_title) { + + override fun perform() { + + if (fragment.context != null && isIgnoringBatteryOptimizations(fragment.context!!)) { + description = fragment.getString(R.string.settings_troubleshoot_test_battery_success) + status = TestStatus.SUCCESS + quickFix = null + } else { + description = fragment.getString(R.string.settings_troubleshoot_test_battery_failed) + quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_battery_quickfix) { + override fun doFix() { + fragment.activity?.let { + requestDisablingBatteryOptimization(it, fragment, NotificationTroubleshootTestManager.REQ_CODE_FIX) + } + } + } + status = TestStatus.FAILED + } + } + +} \ No newline at end of file diff --git a/vector/src/fdroid/java/im/vector/receiver/OnApplicationUpgradeReceiver.java b/vector/src/fdroid/java/im/vector/receiver/OnApplicationUpgradeReceiver.java new file mode 100644 index 00000000..a90ca6b1 --- /dev/null +++ b/vector/src/fdroid/java/im/vector/receiver/OnApplicationUpgradeReceiver.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import timber.log.Timber; + +public class OnApplicationUpgradeReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + Timber.d("## onReceive() : Application has been upgraded, restart event stream service."); + + // Start Event stream + // TODO EventStreamServiceX.Companion.onApplicationUpgrade(context); + } +} diff --git a/vector/src/gplay/AndroidManifest.xml b/vector/src/gplay/AndroidManifest.xml new file mode 100755 index 00000000..e48db667 --- /dev/null +++ b/vector/src/gplay/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/vector/src/gplay/google-services.json b/vector/src/gplay/google-services.json new file mode 100644 index 00000000..8ffc2cef --- /dev/null +++ b/vector/src/gplay/google-services.json @@ -0,0 +1,46 @@ +{ + "project_info": { + "project_number": "912726360885", + "firebase_url": "https://vector-alpha.firebaseio.com", + "project_id": "vector-alpha", + "storage_bucket": "vector-alpha.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:912726360885:android:448c9b63161abc9c", + "android_client_info": { + "package_name": "im.vector.riotredesign" + } + }, + "oauth_client": [ + { + "client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 1, + "other_platform_oauth_client": [] + }, + "ads_service": { + "status": 2 + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/vector/src/gplay/java/im/vector/push/fcm/FcmHelper.java b/vector/src/gplay/java/im/vector/push/fcm/FcmHelper.java new file mode 100755 index 00000000..ff5ed944 --- /dev/null +++ b/vector/src/gplay/java/im/vector/push/fcm/FcmHelper.java @@ -0,0 +1,120 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.push.fcm; + +import android.app.Activity; +import android.content.Context; +import android.preference.PreferenceManager; +import android.text.TextUtils; +import android.widget.Toast; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import im.vector.riotredesign.R; +import timber.log.Timber; + +/** + * This class store the FCM token in SharedPrefs and ensure this token is retrieved. + * It has an alter ego in the fdroid variant. + */ +public class FcmHelper { + private static final String LOG_TAG = FcmHelper.class.getSimpleName(); + + private static final String PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"; + + /** + * Retrieves the FCM registration token. + * + * @return the FCM token or null if not received from FCM + */ + @Nullable + public static String getFcmToken(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getString(PREFS_KEY_FCM_TOKEN, null); + } + + /** + * Store FCM token to the SharedPrefs + * + * @param context android context + * @param token the token to store + */ + public static void storeFcmToken(@NonNull Context context, + @Nullable String token) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putString(PREFS_KEY_FCM_TOKEN, token) + .apply(); + } + + /** + * onNewToken may not be called on application upgrade, so ensure my shared pref is set + * + * @param activity the first launch Activity + */ + public static void ensureFcmTokenIsRetrieved(final Activity activity) { + if (TextUtils.isEmpty(getFcmToken(activity))) { + + + //vfe: according to firebase doc + //'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' + if (checkPlayServices(activity)) { + try { + FirebaseInstanceId.getInstance().getInstanceId() + .addOnSuccessListener(activity, new OnSuccessListener() { + @Override + public void onSuccess(InstanceIdResult instanceIdResult) { + storeFcmToken(activity, instanceIdResult.getToken()); + } + }) + .addOnFailureListener(activity, new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.getMessage()); + } + }); + } catch (Throwable e) { + Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.getMessage()); + } + } else { + Toast.makeText(activity, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show(); + Timber.e("No valid Google Play Services found. Cannot use FCM."); + } + } + } + + /** + * Check the device to make sure it has the Google Play Services APK. If + * it doesn't, display a dialog that allows users to download the APK from + * the Google Play Store or enable it in the device's system settings. + */ + private static boolean checkPlayServices(Activity activity) { + GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance(); + int resultCode = apiAvailability.isGooglePlayServicesAvailable(activity); + if (resultCode != ConnectionResult.SUCCESS) { + return false; + } + return true; + } +} diff --git a/vector/src/gplay/java/im/vector/push/fcm/NotificationTroubleshootTestManagerFactory.kt b/vector/src/gplay/java/im/vector/push/fcm/NotificationTroubleshootTestManagerFactory.kt new file mode 100644 index 00000000..be03c08e --- /dev/null +++ b/vector/src/gplay/java/im/vector/push/fcm/NotificationTroubleshootTestManagerFactory.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.push.fcm + +import androidx.fragment.app.Fragment +import im.vector.fragments.troubleshoot.TestAccountSettings +import im.vector.matrix.android.api.session.Session +import im.vector.push.fcm.troubleshoot.TestFirebaseToken +import im.vector.push.fcm.troubleshoot.TestPlayServices +import im.vector.push.fcm.troubleshoot.TestTokenRegistration +import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager +import im.vector.riotredesign.features.settings.troubleshoot.TestBingRulesSettings +import im.vector.riotredesign.features.settings.troubleshoot.TestDeviceSettings +import im.vector.riotredesign.features.settings.troubleshoot.TestSystemSettings + +class NotificationTroubleshootTestManagerFactory { + + companion object { + fun createTestManager(fragment: Fragment, session: Session?): NotificationTroubleshootTestManager { + val mgr = NotificationTroubleshootTestManager(fragment) + mgr.addTest(TestSystemSettings(fragment)) + if (session != null) { + mgr.addTest(TestAccountSettings(fragment, session)) + } + mgr.addTest(TestDeviceSettings(fragment)) + if (session != null) { + mgr.addTest(TestBingRulesSettings(fragment, session)) + } + mgr.addTest(TestPlayServices(fragment)) + mgr.addTest(TestFirebaseToken(fragment)) + mgr.addTest(TestTokenRegistration(fragment)) + return mgr + } + } + +} \ No newline at end of file diff --git a/vector/src/gplay/java/im/vector/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/push/fcm/VectorFirebaseMessagingService.kt new file mode 100755 index 00000000..1c74d097 --- /dev/null +++ b/vector/src/gplay/java/im/vector/push/fcm/VectorFirebaseMessagingService.kt @@ -0,0 +1,271 @@ +/* + * Copyright 2019 New Vector Ltd + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.push.fcm + +import android.os.Handler +import android.os.Looper +import android.text.TextUtils +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.riotredesign.BuildConfig +import im.vector.riotredesign.R +import im.vector.riotredesign.core.preference.BingRule +import im.vector.riotredesign.features.badge.BadgeProxy +import im.vector.riotredesign.features.notifications.NotifiableEventResolver +import im.vector.riotredesign.features.notifications.NotifiableMessageEvent +import im.vector.riotredesign.features.notifications.NotificationDrawerManager +import im.vector.riotredesign.features.notifications.SimpleNotifiableEvent +import org.koin.android.ext.android.inject +import timber.log.Timber + +/** + * Class extending FirebaseMessagingService. + */ +class VectorFirebaseMessagingService : FirebaseMessagingService() { + + val notificationDrawerManager by inject() + + private val notifiableEventResolver by lazy { + NotifiableEventResolver(this) + } + + // UI handler + private val mUIHandler by lazy { + Handler(Looper.getMainLooper()) + } + + /** + * Called when message is received. + * + * @param message the message + */ + override fun onMessageReceived(message: RemoteMessage?) { + if (message == null || message.data == null) { + Timber.e("## onMessageReceived() : received a null message or message with no data") + return + } + if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { + Timber.i("## onMessageReceived()" + message.data.toString()) + Timber.i("## onMessageReceived() from FCM with priority " + message.priority) + } + + //safe guard + /* TODO + val pushManager = Matrix.getInstance(applicationContext).pushManager + if (!pushManager.areDeviceNotificationsAllowed()) { + Timber.i("## onMessageReceived() : the notifications are disabled") + return + } + */ + + //TODO if the app is in foreground, we could just ignore this. The sync loop is already going? + // TODO mUIHandler.post { onMessageReceivedInternal(message.data, pushManager) } + } + + /** + * Called if InstanceID token is updated. This may occur if the security of + * the previous token had been compromised. Note that this is also called + * when the InstanceID token is initially generated, so this is where + * you retrieve the token. + */ + override fun onNewToken(refreshedToken: String?) { + Timber.i("onNewToken: FCM Token has been updated") + FcmHelper.storeFcmToken(this, refreshedToken) + // TODO Matrix.getInstance(this)?.pushManager?.resetFCMRegistration(refreshedToken) + } + + override fun onDeletedMessages() { + Timber.d("## onDeletedMessages()") + } + + /** + * Internal receive method + * + * @param data Data map containing message data as key/value pairs. + * For Set of keys use data.keySet(). + */ + private fun onMessageReceivedInternal(data: Map /*, pushManager: PushManager*/) { + try { + if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { + Timber.i("## onMessageReceivedInternal() : $data") + } + // update the badge counter + val unreadCount = data.get("unread")?.let { Integer.parseInt(it) } ?: 0 + BadgeProxy.updateBadgeCount(applicationContext, unreadCount) + + /* TODO + val session = Matrix.getInstance(applicationContext)?.defaultSession + + if (VectorApp.isAppInBackground() && !pushManager.isBackgroundSyncAllowed) { + //Notification contains metadata and maybe data information + handleNotificationWithoutSyncingMode(data, session) + } else { + // Safe guard... (race?) + if (isEventAlreadyKnown(data["event_id"], data["room_id"])) return + //Catch up!! + EventStreamServiceX.onPushReceived(this) + } + */ + } catch (e: Exception) { + Timber.e(e, "## onMessageReceivedInternal() failed : " + e.message) + } + } + + // check if the event was not yet received + // a previous catchup might have already retrieved the notified event + private fun isEventAlreadyKnown(eventId: String?, roomId: String?): Boolean { + if (null != eventId && null != roomId) { + try { + /* TODO + val sessions = Matrix.getInstance(applicationContext).sessions + + if (null != sessions && !sessions.isEmpty()) { + for (session in sessions) { + if (session.dataHandler?.store?.isReady == true) { + session.dataHandler.store?.getEvent(eventId, roomId)?.let { + Timber.e("## isEventAlreadyKnown() : ignore the event " + eventId + + " in room " + roomId + " because it is already known") + return true + } + } + } + } + */ + } catch (e: Exception) { + Timber.e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined " + e.message) + } + + } + return false + } + + private fun handleNotificationWithoutSyncingMode(data: Map, session: Session?) { + + if (session == null) { + Timber.e("## handleNotificationWithoutSyncingMode cannot find session") + return + } + + // The Matrix event ID of the event being notified about. + // This is required if the notification is about a particular Matrix event. + // It may be omitted for notifications that only contain updated badge counts. + // This ID can and should be used to detect duplicate notification requests. + val eventId = data["event_id"] ?: return //Just ignore + + + val eventType = data["type"] + if (eventType == null) { + //Just add a generic unknown event + val simpleNotifiableEvent = SimpleNotifiableEvent( + session.sessionParams.credentials.userId, + eventId, + true, //It's an issue in this case, all event will bing even if expected to be silent. + title = getString(R.string.notification_unknown_new_event), + description = "", + type = null, + timestamp = System.currentTimeMillis(), + soundName = BingRule.ACTION_VALUE_DEFAULT, + isPushGatewayEvent = true + ) + notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent) + notificationDrawerManager.refreshNotificationDrawer(null) + + return + } else { + + val event = parseEvent(data) + if (event?.roomId == null) { + //unsupported event + Timber.e("Received an event with no room id") + return + } else { + + var notifiableEvent = notifiableEventResolver.resolveEvent(event, null, null /* TODO session.fulfillRule(event) */, session) + + if (notifiableEvent == null) { + Timber.e("Unsupported notifiable event ${eventId}") + if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { + Timber.e("--> ${event}") + } + } else { + + + if (notifiableEvent is NotifiableMessageEvent) { + if (TextUtils.isEmpty(notifiableEvent.senderName)) { + notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: "" + } + if (TextUtils.isEmpty(notifiableEvent.roomName)) { + notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: "" + } + } + + notifiableEvent.isPushGatewayEvent = true + notifiableEvent.matrixID = session.sessionParams.credentials.userId + notificationDrawerManager.onNotifiableEventReceived(notifiableEvent) + notificationDrawerManager.refreshNotificationDrawer(null) + } + } + } + } + + private fun findRoomNameBestEffort(data: Map, session: Session?): String? { + var roomName: String? = data["room_name"] + val roomId = data["room_id"] + if (null == roomName && null != roomId) { + // Try to get the room name from our store + /* + TODO + if (session?.dataHandler?.store?.isReady == true) { + val room = session.getRoom(roomId) + roomName = room?.getRoomDisplayName(this) + } + */ + } + return roomName + } + + /** + * Try to create an event from the FCM data + * + * @param data the FCM data + * @return the event + */ + private fun parseEvent(data: Map?): Event? { + // accept only event with room id. + if (null == data || !data.containsKey("room_id") || !data.containsKey("event_id")) { + return null + } + + try { + return Event(eventId = data["event_id"], + sender = data["sender"], + roomId = data["room_id"], + type = data.getValue("type"), + // TODO content = data.getValue("content"), + originServerTs = System.currentTimeMillis()) + } catch (e: Exception) { + Timber.e(e, "buildEvent fails " + e.localizedMessage) + } + + return null + } +} diff --git a/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestFirebaseToken.kt b/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestFirebaseToken.kt new file mode 100644 index 00000000..7b3d9be4 --- /dev/null +++ b/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestFirebaseToken.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.push.fcm.troubleshoot + +import androidx.fragment.app.Fragment +import com.google.firebase.iid.FirebaseInstanceId +import im.vector.riotredesign.R +import im.vector.riotredesign.core.utils.startAddGoogleAccountIntent +import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager +import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest +import timber.log.Timber + +/* +* Test that app can successfully retrieve a token via firebase + */ +class TestFirebaseToken(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_fcm_title) { + + override fun perform() { + status = TestStatus.RUNNING + val activity = fragment.activity + if (activity != null) { + try { + FirebaseInstanceId.getInstance().instanceId + .addOnCompleteListener(activity) { task -> + if (!task.isSuccessful) { + val errorMsg = if (task.exception == null) "Unknown" else task.exception!!.localizedMessage + //Can't find where this constant is (not documented -or deprecated in docs- and all obfuscated) + if ("SERVICE_NOT_AVAILABLE".equals(errorMsg)) { + description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg) + } else if ("TOO_MANY_REGISTRATIONS".equals(errorMsg)) { + description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg) + } else if ("ACCOUNT_MISSING".equals(errorMsg)) { + description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed_account_missing, errorMsg) + quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_fcm_failed_account_missing_quick_fix) { + override fun doFix() { + startAddGoogleAccountIntent(fragment, NotificationTroubleshootTestManager.REQ_CODE_FIX) + } + } + } else { + description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed, errorMsg) + } + status = TestStatus.FAILED + } else { + task.result?.token?.let { + val tok = it.substring(0, Math.min(8, it.length)) + "********************" + description = fragment.getString(R.string.settings_troubleshoot_test_fcm_success, tok) + Timber.e("Retrieved FCM token success [$it].") + } + status = TestStatus.SUCCESS + } + } + } catch (e: Throwable) { + description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed, e.localizedMessage) + status = TestStatus.FAILED + } + } else { + status = TestStatus.FAILED + } + } + +} \ No newline at end of file diff --git a/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestPlayServices.kt b/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestPlayServices.kt new file mode 100644 index 00000000..a5f93f4f --- /dev/null +++ b/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestPlayServices.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.push.fcm.troubleshoot + +import androidx.fragment.app.Fragment +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import im.vector.riotredesign.R +import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest +import timber.log.Timber + +/* +* Check that the play services APK is available an up-to-date. If needed provide quick fix to install it. + */ +class TestPlayServices(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_play_services_title) { + + override fun perform() { + val apiAvailability = GoogleApiAvailability.getInstance() + val resultCode = apiAvailability.isGooglePlayServicesAvailable(fragment.context) + if (resultCode == ConnectionResult.SUCCESS) { + quickFix = null + description = fragment.getString(R.string.settings_troubleshoot_test_play_services_success) + status = TestStatus.SUCCESS + } else { + if (apiAvailability.isUserResolvableError(resultCode)) { + quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_play_services_quickfix) { + override fun doFix() { + fragment.activity?.let { + apiAvailability.getErrorDialog(it, resultCode, 9000 /*hey does the magic number*/).show() + } + } + } + Timber.e("Play Services apk error $resultCode -> ${apiAvailability.getErrorString(resultCode)}.") + } + + description = fragment.getString(R.string.settings_troubleshoot_test_play_services_failed, apiAvailability.getErrorString(resultCode)) + status = TestStatus.FAILED + } + } + +} + diff --git a/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestTokenRegistration.kt b/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestTokenRegistration.kt new file mode 100644 index 00000000..38b7375a --- /dev/null +++ b/vector/src/gplay/java/im/vector/push/fcm/troubleshoot/TestTokenRegistration.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.push.fcm.troubleshoot + +import androidx.fragment.app.Fragment +import im.vector.riotredesign.R +import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest + +/** + * Force registration of the token to HomeServer + */ +class TestTokenRegistration(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_token_registration_title) { + + override fun perform() { + /* + TODO + Matrix.getInstance(VectorApp.getInstance().baseContext).pushManager.forceSessionsRegistration(object : ApiCallback { + override fun onSuccess(info: Void?) { + description = fragment.getString(R.string.settings_troubleshoot_test_token_registration_success) + status = TestStatus.SUCCESS + } + + override fun onNetworkError(e: Exception?) { + description = fragment.getString(R.string.settings_troubleshoot_test_token_registration_failed, e?.localizedMessage) + status = TestStatus.FAILED + } + + override fun onMatrixError(e: MatrixError?) { + description = fragment.getString(R.string.settings_troubleshoot_test_token_registration_failed, e?.localizedMessage) + status = TestStatus.FAILED + } + + override fun onUnexpectedError(e: Exception?) { + description = fragment.getString(R.string.settings_troubleshoot_test_token_registration_failed, e?.localizedMessage) + status = TestStatus.FAILED + } + }) + */ + + status = TestStatus.FAILED + + } + +} \ No newline at end of file diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 9155aa00..9e73f7a8 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -32,6 +32,15 @@ + + + + \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt index 9c14a508..3fd69879 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt @@ -26,6 +26,7 @@ import im.vector.riotredesign.features.home.group.SelectedGroupStore import im.vector.riotredesign.features.home.room.VisibleRoomStore import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator +import im.vector.riotredesign.features.notifications.NotificationDrawerManager import org.koin.dsl.module.module class AppModule(private val context: Context) { @@ -64,6 +65,10 @@ class AppModule(private val context: Context) { RoomSummaryComparator() } + single { + NotificationDrawerManager(context) + } + factory { Matrix.getInstance().currentSession!! } diff --git a/vector/src/main/java/im/vector/riotredesign/core/extensions/UrlExtensions.kt b/vector/src/main/java/im/vector/riotredesign/core/extensions/UrlExtensions.kt new file mode 100644 index 00000000..e087c3fd --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/extensions/UrlExtensions.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.extensions + +import java.net.URLEncoder + +/** + * Append param and value to a Url, using "?" or "&". Value parameter will be encoded + * Return this for chaining purpose + */ +fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder { + if (contains("?")) { + append("&") + } else { + append("?") + } + + append(param) + append("=") + append(URLEncoder.encode(value, "utf-8")) + + return this +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/extensions/ViewExtensions.kt b/vector/src/main/java/im/vector/riotredesign/core/extensions/ViewExtensions.kt new file mode 100644 index 00000000..99af0a38 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/extensions/ViewExtensions.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.extensions + +import android.text.InputType +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import androidx.appcompat.widget.SearchView +import im.vector.riotredesign.R + +/** + * Remove left margin of a SearchView + */ +fun SearchView.withoutLeftMargin() { + (findViewById(R.id.search_edit_frame))?.let { + val searchEditFrameParams = it.layoutParams as ViewGroup.MarginLayoutParams + searchEditFrameParams.leftMargin = 0 + it.layoutParams = searchEditFrameParams + } + + (findViewById(R.id.search_mag_icon))?.let { + val searchIconParams = it.layoutParams as ViewGroup.MarginLayoutParams + searchIconParams.leftMargin = 0 + it.layoutParams = searchIconParams + } +} + +fun EditText.showPassword(visible: Boolean, updateCursor: Boolean = true) { + if (visible) { + inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + } else { + inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + } + if (updateCursor) setSelection(text?.length ?: 0) +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/AddressPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/AddressPreference.kt new file mode 100755 index 00000000..e3fb1a07 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/preference/AddressPreference.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.preference + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import androidx.preference.PreferenceViewHolder +import im.vector.riotredesign.R + +/** + * Preference used in Room setting for Room aliases + */ +class AddressPreference : VectorPreference { + + // members + private var mMainAddressIconView: ImageView? = null + private var mIsMainIconVisible = false + + /** + * @return the main icon view. + */ + val mainIconView: View? + get() = mMainAddressIconView + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) + + init { + widgetLayoutResource = R.layout.vector_settings_address_preference + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + + val view = holder.itemView + mMainAddressIconView = view.findViewById(R.id.main_address_icon_view) + mMainAddressIconView!!.visibility = if (mIsMainIconVisible) View.VISIBLE else View.GONE + } + + /** + * Set the main address icon visibility. + * + * @param isVisible true to display the main icon + */ + fun setMainIconVisible(isVisible: Boolean) { + mIsMainIconVisible = isVisible + + mMainAddressIconView?.visibility = if (mIsMainIconVisible) View.VISIBLE else View.GONE + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/BingRulePreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/BingRulePreference.kt new file mode 100755 index 00000000..bd10f709 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/preference/BingRulePreference.kt @@ -0,0 +1,243 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.preference + +import android.content.Context +import android.text.TextUtils +import android.util.AttributeSet +import android.view.View +import android.widget.RadioGroup +import android.widget.TextView +import androidx.preference.PreferenceViewHolder +import im.vector.riotredesign.R + +// TODO Replace by real Bingrule class +class BingRule(rule: BingRule) { + fun shouldNotNotify() = false + fun shouldNotify() = false + fun setNotify(b: Boolean) { + + } + + fun setHighlight(b: Boolean) { + + } + + fun removeNotificationSound() { + + } + + val ruleId: CharSequence? = null + var isEnabled = false + var notificationSound: String? = null + val kind: CharSequence? = null + + companion object { + const val RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS = "TODO" + const val ACTION_VALUE_DEFAULT = "TODO" + const val KIND_UNDERRIDE = "TODO" + const val RULE_ID_INVITE_ME = "TODO" + const val RULE_ID_CALL = "TODO" + const val ACTION_VALUE_RING = "TODO" + const val RULE_ID_DISABLE_ALL = "TODO" + const val ACTION_DONT_NOTIFY = "TODO" + const val RULE_ID_CONTAIN_DISPLAY_NAME = "TODO" + const val RULE_ID_CONTAIN_USER_NAME = "TODO" + const val RULE_ID_ONE_TO_ONE_ROOM = "TODO" + const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = "TODO" + } + +} + +class BingRulePreference : VectorPreference { + + /** + * @return the selected bing rule + */ + var rule: BingRule? = null + private set + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) + + init { + layoutResource = R.layout.vector_preference_bing_rule + } + + /** + * @return the bing rule status index + */ + val ruleStatusIndex: Int + get() { + if (null != rule) { + if (TextUtils.equals(rule!!.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) { + if (rule!!.shouldNotNotify()) { + return if (rule!!.isEnabled) { + NOTIFICATION_OFF_INDEX + } else { + NOTIFICATION_SILENT_INDEX + } + } else if (rule!!.shouldNotify()) { + return NOTIFICATION_NOISY_INDEX + } + } + + if (rule!!.isEnabled) { + return if (rule!!.shouldNotNotify()) { + NOTIFICATION_OFF_INDEX + } else if (null != rule!!.notificationSound) { + NOTIFICATION_NOISY_INDEX + } else { + NOTIFICATION_SILENT_INDEX + } + } + } + + return NOTIFICATION_OFF_INDEX + } + + /** + * Update the bing rule. + * + * @param aBingRule + */ + fun setBingRule(aBingRule: BingRule) { + rule = aBingRule + refreshSummary() + } + + /** + * Refresh the summary + */ + private fun refreshSummary() { + summary = context.getString(when (ruleStatusIndex) { + NOTIFICATION_OFF_INDEX -> R.string.notification_off + NOTIFICATION_SILENT_INDEX -> R.string.notification_silent + else -> R.string.notification_noisy + }) + } + + /** + * Create a bing rule with the updated required at index. + * + * @param index index + * @return a bing rule with the updated flags / null if there is no update + */ + fun createRule(index: Int): BingRule? { + var rule: BingRule? = null + + if (null != this.rule && index != ruleStatusIndex) { + rule = BingRule(this.rule!!) + + if (TextUtils.equals(rule.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) { + when (index) { + NOTIFICATION_OFF_INDEX -> { + rule.isEnabled = true + rule.setNotify(false) + } + NOTIFICATION_SILENT_INDEX -> { + rule.isEnabled = false + rule.setNotify(false) + } + NOTIFICATION_NOISY_INDEX -> { + rule.isEnabled = true + rule.setNotify(true) + rule.notificationSound = BingRule.ACTION_VALUE_DEFAULT + } + } + + return rule + } + + + if (NOTIFICATION_OFF_INDEX == index) { + if (TextUtils.equals(this.rule!!.kind, BingRule.KIND_UNDERRIDE) + || TextUtils.equals(rule.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) { + rule.setNotify(false) + } else { + rule.isEnabled = false + } + } else { + rule.isEnabled = true + rule.setNotify(true) + rule.setHighlight(!TextUtils.equals(this.rule!!.kind, BingRule.KIND_UNDERRIDE) + && !TextUtils.equals(rule.ruleId, BingRule.RULE_ID_INVITE_ME) + && NOTIFICATION_NOISY_INDEX == index) + if (NOTIFICATION_NOISY_INDEX == index) { + rule.notificationSound = if (TextUtils.equals(rule.ruleId, BingRule.RULE_ID_CALL)) + BingRule.ACTION_VALUE_RING + else + BingRule.ACTION_VALUE_DEFAULT + } else { + rule.removeNotificationSound() + } + } + } + + return rule + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + + holder.itemView.findViewById(android.R.id.summary)?.visibility = View.GONE + holder.itemView.setOnClickListener(null) + holder.itemView.setOnLongClickListener(null) + + val radioGroup = holder.findViewById(R.id.bingPreferenceRadioGroup) as? RadioGroup + radioGroup?.setOnCheckedChangeListener(null) + + when (ruleStatusIndex) { + NOTIFICATION_OFF_INDEX -> { + radioGroup?.check(R.id.bingPreferenceRadioBingRuleOff) + } + NOTIFICATION_SILENT_INDEX -> { + radioGroup?.check(R.id.bingPreferenceRadioBingRuleSilent) + } + else -> { + radioGroup?.check(R.id.bingPreferenceRadioBingRuleNoisy) + } + } + + radioGroup?.setOnCheckedChangeListener { group, checkedId -> + when (checkedId) { + R.id.bingPreferenceRadioBingRuleOff -> { + onPreferenceChangeListener?.onPreferenceChange(this, NOTIFICATION_OFF_INDEX) + } + R.id.bingPreferenceRadioBingRuleSilent -> { + onPreferenceChangeListener?.onPreferenceChange(this, NOTIFICATION_SILENT_INDEX) + } + R.id.bingPreferenceRadioBingRuleNoisy -> { + onPreferenceChangeListener?.onPreferenceChange(this, NOTIFICATION_NOISY_INDEX) + } + } + } + + } + + + companion object { + + // index in mRuleStatuses + private const val NOTIFICATION_OFF_INDEX = 0 + private const val NOTIFICATION_SILENT_INDEX = 1 + private const val NOTIFICATION_NOISY_INDEX = 2 + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/ProgressBarPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/ProgressBarPreference.kt new file mode 100755 index 00000000..de36106c --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/preference/ProgressBarPreference.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.preference + +import android.content.Context +import android.util.AttributeSet +import androidx.preference.Preference +import im.vector.riotredesign.R + +class ProgressBarPreference : Preference { + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) + + init { + layoutResource = R.layout.vector_settings_spinner_preference + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/RoomAvatarPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/RoomAvatarPreference.kt new file mode 100644 index 00000000..9c773607 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/preference/RoomAvatarPreference.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.preference + +import android.content.Context +import android.util.AttributeSet +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.Room + +/** + * Specialized class to target a Room avatar preference. + * Based don the avatar preference class it redefines refreshAvatar() and + * add the new method setConfiguration(). + */ +class RoomAvatarPreference : UserAvatarPreference { + + private var mRoom: Room? = null + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) + + override fun refreshAvatar() { + if (null != mAvatarView && null != mRoom) { + // TODO + // VectorUtils.loadRoomAvatar(context, mSession, mAvatarView, mRoom) + } + } + + fun setConfiguration(aSession: Session, aRoom: Room) { + mSession = aSession + mRoom = aRoom + refreshAvatar() + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/UserAvatarPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/UserAvatarPreference.kt new file mode 100755 index 00000000..0514e516 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/preference/UserAvatarPreference.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.preference + +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageView +import android.widget.ProgressBar +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import im.vector.matrix.android.api.session.Session +import im.vector.riotredesign.R + +open class UserAvatarPreference : Preference { + + internal var mAvatarView: ImageView? = null + internal var mSession: Session? = null + private var mLoadingProgressBar: ProgressBar? = null + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) + + init { + widgetLayoutResource = R.layout.vector_settings_round_avatar + isIconSpaceReserved = false + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + + mAvatarView = holder.itemView.findViewById(R.id.settings_avatar) + mLoadingProgressBar = holder.itemView.findViewById(R.id.avatar_update_progress_bar) + refreshAvatar() + } + + open fun refreshAvatar() { + if (null != mAvatarView && null != mSession) { + // TODO + // val myUser = mSession!!.myUser + // VectorUtils.loadUserAvatar(context, mSession, mAvatarView, myUser.avatarUrl, myUser.user_id, myUser.displayname) + } + } + + fun setSession(session: Session) { + mSession = session + refreshAvatar() + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorEditTextPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorEditTextPreference.kt new file mode 100644 index 00000000..973857b1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorEditTextPreference.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.preference + +import android.content.Context +import android.util.AttributeSet +import android.widget.TextView +import androidx.preference.EditTextPreference +import androidx.preference.PreferenceViewHolder +import im.vector.riotredesign.R +import timber.log.Timber + +/** + * Use this class to create an EditTextPreference form code and avoid a crash (see https://code.google.com/p/android/issues/detail?id=231576) + */ +class VectorEditTextPreference : EditTextPreference { + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) + + init { + dialogLayoutResource = R.layout.dialog_preference_edit_text + isIconSpaceReserved = false + } + + // No single line for title + override fun onBindViewHolder(holder: PreferenceViewHolder) { + // display the title in multi-line to avoid ellipsis. + try { + holder.itemView.findViewById(android.R.id.title)?.setSingleLine(false) + } catch (e: Exception) { + Timber.e(e, "onBindView " + e.message) + } + + super.onBindViewHolder(holder) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorGroupPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorGroupPreference.kt new file mode 100644 index 00000000..553fd07e --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorGroupPreference.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.preference + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.preference.PreferenceViewHolder +import androidx.preference.SwitchPreference +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.group.Group +import im.vector.riotredesign.R + +class VectorGroupPreference : SwitchPreference { + + private var mAvatarView: ImageView? = null + + private var mGroup: Group? = null + private var mSession: Session? = null + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + + val createdView = holder.itemView + + if (mAvatarView == null) { + try { + // insert the group avatar to the left + val iconView = createdView.findViewById(android.R.id.icon) + + var iconViewParent = iconView.parent + + while (null != iconViewParent.parent) { + iconViewParent = iconViewParent.parent + } + + val inflater = LayoutInflater.from(context) + val layout = inflater.inflate(R.layout.vector_settings_round_group_avatar, (iconViewParent as LinearLayout), false) as FrameLayout + mAvatarView = layout.findViewById(R.id.settings_round_group_avatar) + + val params = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) + params.gravity = Gravity.CENTER + layout.layoutParams = params + iconViewParent.addView(layout, 0) + + } catch (e: Exception) { + mAvatarView = null + } + + } + + refreshAvatar() + } + + /** + * Init the group information + * + * @param group the group + * @param session the session + */ + fun setGroup(group: Group, session: Session) { + mGroup = group + mSession = session + + refreshAvatar() + } + + /** + * Refresh the avatar + */ + private fun refreshAvatar() { + if (null != mAvatarView && null != mSession && null != mGroup) { + // TODO + // VectorUtils.loadGroupAvatar(context, mSession, mAvatarView, mGroup) + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorListPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorListPreference.kt new file mode 100644 index 00000000..5ea5db36 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorListPreference.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.preference + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import im.vector.riotredesign.R + +/** + * Customize ListPreference class to add a warning icon to the right side of the list. + */ +class VectorListPreference : ListPreference { + + // + private var mWarningIconView: View? = null + private var mIsWarningIconVisible = false + private var mWarningIconClickListener: OnPreferenceWarningIconClickListener? = null + + /** + * Interface definition for a callback to be invoked when the warning icon is clicked. + */ + interface OnPreferenceWarningIconClickListener { + /** + * Called when a warning icon has been clicked. + * + * @param preference The Preference that was clicked. + */ + fun onWarningIconClick(preference: Preference) + } + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + init { + widgetLayoutResource = R.layout.vector_settings_list_preference_with_warning + isIconSpaceReserved = false + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + + val view = holder.itemView + + mWarningIconView = view.findViewById(R.id.list_preference_warning_icon) + mWarningIconView!!.visibility = if (mIsWarningIconVisible) View.VISIBLE else View.GONE + + mWarningIconView!!.setOnClickListener { + if (null != mWarningIconClickListener) { + mWarningIconClickListener!!.onWarningIconClick(this@VectorListPreference) + } + } + } + + /** + * Sets the callback to be invoked when this warning icon is clicked. + * + * @param onPreferenceWarningIconClickListener The callback to be invoked. + */ + fun setOnPreferenceWarningIconClickListener(onPreferenceWarningIconClickListener: OnPreferenceWarningIconClickListener) { + mWarningIconClickListener = onPreferenceWarningIconClickListener + } + + /** + * Set the warning icon visibility. + * + * @param isVisible to display the icon + */ + fun setWarningIconVisible(isVisible: Boolean) { + mIsWarningIconVisible = isVisible + + if (null != mWarningIconView) { + mWarningIconView!!.visibility = if (mIsWarningIconVisible) View.VISIBLE else View.GONE + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreference.kt new file mode 100755 index 00000000..6b46a633 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreference.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.preference + +import android.animation.Animator +import android.animation.ArgbEvaluator +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Color +import android.graphics.Typeface +import android.util.AttributeSet +import android.view.View +import android.widget.TextView +import androidx.core.animation.doOnEnd +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import im.vector.riotredesign.R +import im.vector.riotredesign.features.themes.ThemeUtils +import timber.log.Timber + + +/** + * create a Preference with a dedicated click/long click methods. + * It also allow the title to be displayed on several lines + */ +open class VectorPreference : Preference { + + var mTypeface = Typeface.NORMAL + + // long press listener + /** + * Returns the callback to be invoked when this Preference is long clicked. + * + * @return The callback to be invoked. + */ + /** + * Sets the callback to be invoked when this Preference is long clicked. + * + * @param onPreferenceLongClickListener The callback to be invoked. + */ + var onPreferenceLongClickListener: OnPreferenceLongClickListener? = null + + /** + * Interface definition for a callback to be invoked when a preference is + * long clicked. + */ + interface OnPreferenceLongClickListener { + /** + * Called when a Preference has been clicked. + * + * @param preference The Preference that was clicked. + * @return True if the click was handled. + */ + fun onPreferenceLongClick(preference: Preference): Boolean + } + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) + + init { + isIconSpaceReserved = false + } + + var isHighlighted = false + set(value) { + field = value + notifyChanged() + } + + var currentHighlightAnimator: Animator? = null + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + val itemView = holder.itemView + addClickListeners(itemView) + + // display the title in multi-line to avoid ellipsis. + try { + val title = itemView.findViewById(android.R.id.title) + val summary = itemView.findViewById(android.R.id.summary) + if (title != null) { + title.setSingleLine(false) + title.setTypeface(null, mTypeface) + } + + if (title !== summary) { + summary.setTypeface(null, mTypeface) + } + + //cancel existing animation (find a way to resume if happens during anim?) + currentHighlightAnimator?.cancel() + if (isHighlighted) { + val colorFrom = Color.TRANSPARENT + val colorTo = ThemeUtils.getColor(itemView.context, R.attr.colorAccent) + currentHighlightAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorFrom, colorTo).apply { + duration = 250 // milliseconds + addUpdateListener { animator -> + itemView?.setBackgroundColor(animator.animatedValue as Int) + } + doOnEnd { + currentHighlightAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorTo, colorFrom).apply { + duration = 250 // milliseconds + addUpdateListener { animator -> + itemView?.setBackgroundColor(animator.animatedValue as Int) + } + doOnEnd { + isHighlighted = false + } + start() + } + } + startDelay = 200 + start() + } + } else { + itemView.setBackgroundColor(Color.TRANSPARENT) + } + + } catch (e: Exception) { + Timber.e(LOG_TAG, "onBindView " + e.message, e) + } + + super.onBindViewHolder(holder) + } + + /** + * @param view + */ + private fun addClickListeners(view: View) { + view.setOnLongClickListener { + if (null != onPreferenceLongClickListener) { + onPreferenceLongClickListener!!.onPreferenceLongClick(this@VectorPreference) + } else false + } + + view.setOnClickListener { + // call only the click listener + if (onPreferenceClickListener != null) { + onPreferenceClickListener.onPreferenceClick(this@VectorPreference) + } + } + } + + companion object { + private val LOG_TAG = VectorPreference::class.java.simpleName + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreferenceCategory.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreferenceCategory.kt new file mode 100644 index 00000000..ebe49127 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreferenceCategory.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.preference + +import android.content.Context +import android.graphics.Typeface +import android.util.AttributeSet +import android.view.ViewGroup +import android.widget.TextView +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceViewHolder + +/** + * Customize PreferenceCategory class to redefine some attributes. + */ +class VectorPreferenceCategory : PreferenceCategory { + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + init { + isIconSpaceReserved = false + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + + val titleTextView = holder.itemView.findViewById(android.R.id.title) + + titleTextView?.setTypeface(null, Typeface.BOLD) + + // "isIconSpaceReserved = false" does not work for preference category, so remove the padding + (titleTextView?.parent as? ViewGroup)?.setPadding(0, 0, 0, 0) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreferenceDivider.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreferenceDivider.kt new file mode 100644 index 00000000..7cb1ec4c --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreferenceDivider.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.preference + +import android.content.Context +import android.util.AttributeSet +import androidx.preference.Preference +import im.vector.riotredesign.R + +/** + * Divider for Preference screen + */ +class VectorPreferenceDivider @JvmOverloads constructor(context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : Preference(context, attrs, defStyleAttr, defStyleRes) { + + init { + layoutResource = R.layout.vector_preference_divider + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorSwitchPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorSwitchPreference.kt new file mode 100644 index 00000000..27af3455 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorSwitchPreference.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.preference + +import android.content.Context +import android.util.AttributeSet +import android.widget.TextView +import androidx.preference.PreferenceViewHolder +import androidx.preference.SwitchPreference + +/** + * Switch preference with title on multiline (only used in XML) + */ +class VectorSwitchPreference : SwitchPreference { + + // Note: @JvmOverload does not work here... + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context) : super(context) + + init { + isIconSpaceReserved = false + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + // display the title in multi-line to avoid ellipsis. + holder.itemView.findViewById(android.R.id.title)?.setSingleLine(false) + + super.onBindViewHolder(holder) + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/CallService.kt b/vector/src/main/java/im/vector/riotredesign/core/services/CallService.kt new file mode 100644 index 00000000..cd9f1329 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/services/CallService.kt @@ -0,0 +1,207 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.services + +import android.content.Context +import android.content.Intent +import androidx.core.content.ContextCompat +import im.vector.riotredesign.features.notifications.NotificationUtils +import timber.log.Timber + +/** + * Foreground service to manage calls + */ +class CallService : VectorService() { + + /** + * call in progress (foreground notification) + */ + private var mCallIdInProgress: String? = null + + /** + * incoming (foreground notification) + */ + private var mIncomingCallId: String? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent == null) { + // Service started again by the system. + // TODO What do we do here? + return START_STICKY + } + + when (intent.action) { + ACTION_INCOMING_CALL -> displayIncomingCallNotification(intent) + ACTION_PENDING_CALL -> displayCallInProgressNotification(intent) + ACTION_NO_ACTIVE_CALL -> hideCallNotifications() + else -> + // Should not happen + myStopSelf() + } + + // We want the system to restore the service if killed + return START_STICKY + } + + //================================================================================ + // Call notification management + //================================================================================ + + /** + * Display a permanent notification when there is an incoming call. + * + * @param session the session + * @param isVideo true if this is a video call, false for voice call + * @param room the room + * @param callId the callId + */ + private fun displayIncomingCallNotification(intent: Intent) { + Timber.d("displayIncomingCallNotification") + + // TODO + /* + + // the incoming call in progress is already displayed + if (!TextUtils.isEmpty(mIncomingCallId)) { + Timber.d("displayIncomingCallNotification : the incoming call in progress is already displayed") + } else if (!TextUtils.isEmpty(mCallIdInProgress)) { + Timber.d("displayIncomingCallNotification : a 'call in progress' notification is displayed") + } else if (null == CallsManager.getSharedInstance().activeCall) { + val callId = intent.getStringExtra(EXTRA_CALL_ID) + + Timber.d("displayIncomingCallNotification : display the dedicated notification") + val notification = NotificationUtils.buildIncomingCallNotification( + this, + intent.getBooleanExtra(EXTRA_IS_VIDEO, false), + intent.getStringExtra(EXTRA_ROOM_NAME), + intent.getStringExtra(EXTRA_MATRIX_ID), + callId) + startForeground(NOTIFICATION_ID, notification) + + mIncomingCallId = callId + + // turn the screen on for 3 seconds + if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) { + try { + val pm = getSystemService(Context.POWER_SERVICE) as PowerManager + val wl = pm.newWakeLock( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP, + CallService::class.java.simpleName) + wl.acquire(3000) + wl.release() + } catch (re: RuntimeException) { + Timber.e(re, "displayIncomingCallNotification : failed to turn screen on ") + } + + } + } else { + Timber.i("displayIncomingCallNotification : do not display the incoming call notification because there is a pending call") + }// test if there is no active call + */ + } + + /** + * Display a call in progress notification. + */ + private fun displayCallInProgressNotification(intent: Intent) { + val callId = intent.getStringExtra(EXTRA_CALL_ID) + + val notification = NotificationUtils.buildPendingCallNotification(applicationContext, + intent.getBooleanExtra(EXTRA_IS_VIDEO, false), + intent.getStringExtra(EXTRA_ROOM_NAME), + intent.getStringExtra(EXTRA_ROOM_ID), + intent.getStringExtra(EXTRA_MATRIX_ID), + callId) + + startForeground(NOTIFICATION_ID, notification) + + mCallIdInProgress = callId + } + + /** + * Hide the permanent call notifications + */ + private fun hideCallNotifications() { + val notification = NotificationUtils.buildCallEndedNotification(applicationContext) + + // It's mandatory to startForeground to avoid crash + startForeground(NOTIFICATION_ID, notification) + + myStopSelf() + } + + companion object { + private const val NOTIFICATION_ID = 6480 + + private const val ACTION_INCOMING_CALL = "im.vector.riotredesign.core.services.CallService.INCOMING_CALL" + private const val ACTION_PENDING_CALL = "im.vector.riotredesign.core.services.CallService.PENDING_CALL" + private const val ACTION_NO_ACTIVE_CALL = "im.vector.riotredesign.core.services.CallService.NO_ACTIVE_CALL" + + private const val EXTRA_IS_VIDEO = "EXTRA_IS_VIDEO" + private const val EXTRA_ROOM_NAME = "EXTRA_ROOM_NAME" + private const val EXTRA_ROOM_ID = "EXTRA_ROOM_ID" + private const val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID" + private const val EXTRA_CALL_ID = "EXTRA_CALL_ID" + + fun onIncomingCall(context: Context, + isVideo: Boolean, + roomName: String, + roomId: String, + matrixId: String, + callId: String) { + val intent = Intent(context, CallService::class.java) + .apply { + action = ACTION_INCOMING_CALL + putExtra(EXTRA_IS_VIDEO, isVideo) + putExtra(EXTRA_ROOM_NAME, roomName) + putExtra(EXTRA_ROOM_ID, roomId) + putExtra(EXTRA_MATRIX_ID, matrixId) + putExtra(EXTRA_CALL_ID, callId) + } + + ContextCompat.startForegroundService(context, intent) + } + + fun onPendingCall(context: Context, + isVideo: Boolean, + roomName: String, + roomId: String, + matrixId: String, + callId: String) { + val intent = Intent(context, CallService::class.java) + .apply { + action = ACTION_PENDING_CALL + putExtra(EXTRA_IS_VIDEO, isVideo) + putExtra(EXTRA_ROOM_NAME, roomName) + putExtra(EXTRA_ROOM_ID, roomId) + putExtra(EXTRA_MATRIX_ID, matrixId) + putExtra(EXTRA_CALL_ID, callId) + } + + ContextCompat.startForegroundService(context, intent) + } + + fun onNoActiveCall(context: Context) { + val intent = Intent(context, CallService::class.java) + .apply { + action = ACTION_NO_ACTIVE_CALL + } + + ContextCompat.startForegroundService(context, intent) + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/EventStreamServiceX.kt b/vector/src/main/java/im/vector/riotredesign/core/services/EventStreamServiceX.kt new file mode 100644 index 00000000..30606329 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/services/EventStreamServiceX.kt @@ -0,0 +1,583 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.services + +import android.content.Context +import android.content.Intent +import androidx.core.content.ContextCompat +import androidx.work.Constraints +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.riotredesign.R +import im.vector.riotredesign.features.notifications.NotifiableEventResolver +import im.vector.riotredesign.features.notifications.NotificationUtils +import org.koin.android.ext.android.inject +import timber.log.Timber +import java.util.concurrent.TimeUnit + +/** + * A service in charge of controlling whether the event stream is running or not. + * + * It manages messages notifications displayed to the end user. + */ +class EventStreamServiceX : VectorService() { + + /** + * Managed session (no multi session for Riot) + */ + private val mSession by inject() + + /** + * Set to true to simulate a push immediately when service is destroyed + */ + private var mSimulatePushImmediate = false + + /** + * The current state. + */ + private var serviceState = ServiceState.INIT + set(newServiceState) { + Timber.i("setServiceState from $field to $newServiceState") + field = newServiceState + } + + /** + * Push manager + */ + // TODO private var mPushManager: PushManager? = null + + private var mNotifiableEventResolver: NotifiableEventResolver? = null + + /** + * Live events listener + */ + /* TODO + private val mEventsListener = object : MXEventListener() { + override fun onBingEvent(event: Event, roomState: RoomState, bingRule: BingRule) { + if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { + Timber.i("%%%%%%%% MXEventListener: the event $event") + } + + Timber.i("prepareNotification : " + event.eventId + " in " + roomState.roomId) + val session = Matrix.getMXSession(applicationContext, event.matrixId) + + // invalid session ? + // should never happen. + // But it could be triggered because of multi accounts management. + // The dedicated account is removing but some pushes are still received. + if (null == session || !session.isAlive) { + Timber.i("prepareNotification : don't bing - no session") + return + } + + if (EventType.CALL_INVITE == event.type) { + handleCallInviteEvent(event) + return + } + + + val notifiableEvent = mNotifiableEventResolver!!.resolveEvent(event, roomState, bingRule, session) + if (notifiableEvent != null) { + VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableEvent) + } + } + + override fun onLiveEventsChunkProcessed(fromToken: String, toToken: String) { + Timber.i("%%%%%%%% MXEventListener: onLiveEventsChunkProcessed[$fromToken->$toToken]") + + VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(OutdatedEventDetector(this@EventStreamServiceX)) + + // do not suspend the application if there is some active calls + if (ServiceState.CATCHUP == serviceState) { + val hasActiveCalls = mSession?.mCallsManager?.hasActiveCalls() == true + + // if there are some active calls, the catchup should not be stopped. + // because an user could answer to a call from another device. + // there will no push because it is his own message. + // so, the client has no choice to catchup until the ring is shutdown + if (hasActiveCalls) { + Timber.i("onLiveEventsChunkProcessed : Catchup again because there are active calls") + catchup(false) + } else if (ServiceState.CATCHUP == serviceState) { + Timber.i("onLiveEventsChunkProcessed : no Active call") + CallsManager.getSharedInstance().checkDeadCalls() + stop() + } + } + } + } + */ + + /** + * Service internal state + */ + private enum class ServiceState { + // Initial state + INIT, + // Service is started for a Catchup. Once the catchup is finished the service will be stopped + CATCHUP, + // Service is started, and session is monitored + STARTED + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + // Cancel any previous worker + cancelAnySimulatedPushSchedule() + + // no intent : restarted by Android + if (null == intent) { + // Cannot happen anymore + Timber.e("onStartCommand : null intent") + myStopSelf() + return START_NOT_STICKY + } + + val action = intent.action + + Timber.i("onStartCommand with action : $action (current state $serviceState)") + + // Manage foreground notification + when (action) { + ACTION_BOOT_COMPLETE, + ACTION_APPLICATION_UPGRADE, + ACTION_SIMULATED_PUSH_RECEIVED -> { + // Display foreground notification + Timber.i("startForeground") + val notification = NotificationUtils.buildForegroundServiceNotification(this, R.string.notification_sync_in_progress) + startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification) + } + ACTION_GO_TO_FOREGROUND -> { + // Stop foreground notification display + Timber.i("stopForeground") + stopForeground(true) + } + } + + if (null == mSession) { + Timber.e("onStartCommand : no sessions") + myStopSelf() + return START_NOT_STICKY + } + + when (action) { + ACTION_START, + ACTION_GO_TO_FOREGROUND -> + when (serviceState) { + ServiceState.INIT -> + start(false) + ServiceState.CATCHUP -> + // A push has been received before, just change state, to avoid stopping the service when catchup is over + serviceState = ServiceState.STARTED + ServiceState.STARTED -> { + // Nothing to do + } + } + ACTION_STOP, + ACTION_GO_TO_BACKGROUND, + ACTION_LOGOUT -> + stop() + ACTION_PUSH_RECEIVED, + ACTION_SIMULATED_PUSH_RECEIVED -> + when (serviceState) { + ServiceState.INIT -> + start(true) + ServiceState.CATCHUP -> + catchup(true) + ServiceState.STARTED -> + // Nothing to do + Unit + } + ACTION_PUSH_UPDATE -> pushStatusUpdate() + ACTION_BOOT_COMPLETE -> { + // No FCM only + mSimulatePushImmediate = true + stop() + } + ACTION_APPLICATION_UPGRADE -> { + // FDroid only + catchup(true) + } + else -> { + // Should not happen + } + } + + // We don't want the service to be restarted automatically by the System + return START_NOT_STICKY + } + + override fun onDestroy() { + super.onDestroy() + + // Schedule worker? + scheduleSimulatedPushIfNeeded() + } + + /** + * Tell the WorkManager to cancel any schedule of push simulation + */ + private fun cancelAnySimulatedPushSchedule() { + WorkManager.getInstance().cancelAllWorkByTag(PUSH_SIMULATOR_REQUEST_TAG) + } + + /** + * Configure the WorkManager to schedule a simulated push, if necessary + */ + private fun scheduleSimulatedPushIfNeeded() { + if (shouldISimulatePush()) { + val delay = if (mSimulatePushImmediate) 0 else 60_000 // TODO mPushManager?.backgroundSyncDelay ?: let { 60_000 } + Timber.i("## service is schedule to restart in $delay millis, if network is connected") + + val pushSimulatorRequest = OneTimeWorkRequestBuilder() + .setInitialDelay(delay.toLong(), TimeUnit.MILLISECONDS) + .setConstraints(Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build()) + .addTag(PUSH_SIMULATOR_REQUEST_TAG) + .build() + + WorkManager.getInstance().let { + // Cancel any previous worker + it.cancelAllWorkByTag(PUSH_SIMULATOR_REQUEST_TAG) + it.enqueue(pushSimulatorRequest) + } + } + } + + /** + * Start the even stream. + * + * @param session the session + */ + private fun startEventStream(session: Session) { + /* TODO + // resume if it was only suspended + if (null != session.currentSyncToken) { + session.resumeEventStream() + } else { + session.startEventStream(store?.eventStreamToken) + } + */ + } + + /** + * Monitor the provided session. + * + * @param session the session + */ + private fun monitorSession(session: Session) { + /* TODO + session.dataHandler.addListener(mEventsListener) + CallsManager.getSharedInstance().addSession(session) + + val store = session.dataHandler.store + + // the store is ready (no data loading in progress...) + if (store!!.isReady) { + startEventStream(session, store) + } else { + // wait that the store is ready before starting the events stream + store.addMXStoreListener(object : MXStoreListener() { + override fun onStoreReady(accountId: String) { + startEventStream(session, store) + + store.removeMXStoreListener(this) + } + + override fun onStoreCorrupted(accountId: String, description: String) { + // start a new initial sync + if (null == store.eventStreamToken) { + startEventStream(session, store) + } else { + // the data are out of sync + Matrix.getInstance(applicationContext)!!.reloadSessions(applicationContext) + } + + store.removeMXStoreListener(this) + } + + override fun onStoreOOM(accountId: String, description: String) { + val uiHandler = Handler(mainLooper) + + uiHandler.post { + Toast.makeText(applicationContext, "$accountId : $description", Toast.LENGTH_LONG).show() + Matrix.getInstance(applicationContext)!!.reloadSessions(applicationContext) + } + } + }) + + store.open() + } + */ + } + + /** + * internal start. + */ + private fun start(forPush: Boolean) { + val applicationContext = applicationContext + // TODO mPushManager = Matrix.getInstance(applicationContext)!!.pushManager + mNotifiableEventResolver = NotifiableEventResolver(applicationContext) + + monitorSession(mSession!!) + + serviceState = if (forPush) { + ServiceState.CATCHUP + } else { + ServiceState.STARTED + } + } + + /** + * internal stop. + */ + private fun stop() { + Timber.i("## stop(): the service is stopped") + + /* TODO + if (null != mSession && mSession!!.isAlive) { + mSession!!.stopEventStream() + mSession!!.dataHandler.removeListener(mEventsListener) + CallsManager.getSharedInstance().removeSession(mSession) + } + mSession = null + */ + + // Stop the service + myStopSelf() + } + + /** + * internal catchup method. + * + * @param checkState true to check if the current state allow to perform a catchup + */ + private fun catchup(checkState: Boolean) { + var canCatchup = true + + if (!checkState) { + Timber.i("catchup without checking serviceState ") + } else { + Timber.i("catchup with serviceState " + serviceState + " CurrentActivity ") // TODO + VectorApp.getCurrentActivity()) + + /* TODO + // the catchup should only be done + // 1- the serviceState is in catchup : the event stream might have gone to sleep between two catchups + // 2- the thread is suspended + // 3- the application has been launched by a push so there is no displayed activity + canCatchup = (serviceState == ServiceState.CATCHUP + //|| (serviceState == ServiceState.PAUSE) + || ServiceState.STARTED == serviceState && null == VectorApp.getCurrentActivity()) + */ + } + + if (canCatchup) { + if (mSession != null) { + // TODO mSession!!.catchupEventStream() + } else { + Timber.i("catchup no session") + } + + serviceState = ServiceState.CATCHUP + } else { + Timber.i("No catchup is triggered because there is already a running event thread") + } + } + + /** + * The push status has been updated (i.e disabled or enabled). + * TODO Useless now? + */ + private fun pushStatusUpdate() { + Timber.i("## pushStatusUpdate") + } + + /* ========================================================================================== + * Push simulator + * ========================================================================================== */ + + /** + * @return true if the FCM is disable or not setup, user allowed background sync, user wants notification + */ + private fun shouldISimulatePush(): Boolean { + return false + + /* TODO + + if (Matrix.getInstance(applicationContext)?.defaultSession == null) { + Timber.i("## shouldISimulatePush: NO: no session") + + return false + } + + mPushManager?.let { pushManager -> + if (pushManager.useFcm() + && !TextUtils.isEmpty(pushManager.currentRegistrationToken) + && pushManager.isServerRegistered) { + // FCM is ok + Timber.i("## shouldISimulatePush: NO: FCM is up") + return false + } + + if (!pushManager.isBackgroundSyncAllowed) { + // User has disabled background sync + Timber.i("## shouldISimulatePush: NO: background sync not allowed") + return false + } + + if (!pushManager.areDeviceNotificationsAllowed()) { + // User does not want notifications + Timber.i("## shouldISimulatePush: NO: user does not want notification") + return false + } + } + + // Lets simulate push + Timber.i("## shouldISimulatePush: YES") + return true + */ + } + + + //================================================================================ + // Call management + //================================================================================ + + private fun handleCallInviteEvent(event: Event) { + /* + TODO + val session = Matrix.getMXSession(applicationContext, event.matrixId) + + // invalid session ? + // should never happen. + // But it could be triggered because of multi accounts management. + // The dedicated account is removing but some pushes are still received. + if (null == session || !session.isAlive) { + Timber.d("prepareCallNotification : don't bing - no session") + return + } + + val room: Room? = session.dataHandler.getRoom(event.roomId) + + // invalid room ? + if (null == room) { + Timber.i("prepareCallNotification : don't bing - the room does not exist") + return + } + + var callId: String? = null + var isVideo = false + + try { + callId = event.contentAsJsonObject?.get("call_id")?.asString + + // Check if it is a video call + val offer = event.contentAsJsonObject?.get("offer")?.asJsonObject + val sdp = offer?.get("sdp") + val sdpValue = sdp?.asString + + isVideo = sdpValue?.contains("m=video") == true + } catch (e: Exception) { + Timber.e("prepareNotification : getContentAsJsonObject " + e.message, e) + } + + if (!TextUtils.isEmpty(callId)) { + CallService.onIncomingCall(this, + isVideo, + room.getRoomDisplayName(this), + room.roomId, + session.myUserId!!, + callId!!) + } + */ + } + + companion object { + private const val PUSH_SIMULATOR_REQUEST_TAG = "PUSH_SIMULATOR_REQUEST_TAG" + + private const val ACTION_START = "im.vector.riotredesign.core.services.EventStreamServiceX.START" + private const val ACTION_LOGOUT = "im.vector.riotredesign.core.services.EventStreamServiceX.LOGOUT" + private const val ACTION_GO_TO_FOREGROUND = "im.vector.riotredesign.core.services.EventStreamServiceX.GO_TO_FOREGROUND" + private const val ACTION_GO_TO_BACKGROUND = "im.vector.riotredesign.core.services.EventStreamServiceX.GO_TO_BACKGROUND" + private const val ACTION_PUSH_UPDATE = "im.vector.riotredesign.core.services.EventStreamServiceX.PUSH_UPDATE" + private const val ACTION_PUSH_RECEIVED = "im.vector.riotredesign.core.services.EventStreamServiceX.PUSH_RECEIVED" + private const val ACTION_SIMULATED_PUSH_RECEIVED = "im.vector.riotredesign.core.services.EventStreamServiceX.SIMULATED_PUSH_RECEIVED" + private const val ACTION_STOP = "im.vector.riotredesign.core.services.EventStreamServiceX.STOP" + private const val ACTION_BOOT_COMPLETE = "im.vector.riotredesign.core.services.EventStreamServiceX.BOOT_COMPLETE" + private const val ACTION_APPLICATION_UPGRADE = "im.vector.riotredesign.core.services.EventStreamServiceX.APPLICATION_UPGRADE" + + /* ========================================================================================== + * Events sent to the service + * ========================================================================================== */ + + fun onApplicationStarted(context: Context) { + sendAction(context, ACTION_START) + } + + fun onLogout(context: Context) { + sendAction(context, ACTION_LOGOUT) + } + + fun onAppGoingToForeground(context: Context) { + sendAction(context, ACTION_GO_TO_FOREGROUND) + } + + fun onAppGoingToBackground(context: Context) { + sendAction(context, ACTION_GO_TO_BACKGROUND) + } + + fun onPushUpdate(context: Context) { + sendAction(context, ACTION_PUSH_UPDATE) + } + + fun onPushReceived(context: Context) { + sendAction(context, ACTION_PUSH_RECEIVED) + } + + fun onSimulatedPushReceived(context: Context) { + sendAction(context, ACTION_SIMULATED_PUSH_RECEIVED, true) + } + + fun onApplicationStopped(context: Context) { + sendAction(context, ACTION_STOP) + } + + fun onBootComplete(context: Context) { + sendAction(context, ACTION_BOOT_COMPLETE, true) + } + + fun onApplicationUpgrade(context: Context) { + sendAction(context, ACTION_APPLICATION_UPGRADE, true) + } + + private fun sendAction(context: Context, action: String, foreground: Boolean = false) { + Timber.i("sendAction $action") + + val intent = Intent(context, EventStreamServiceX::class.java) + intent.action = action + + if (foreground) { + ContextCompat.startForegroundService(context, intent) + } else { + context.startService(intent) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/PushSimulatorWorker.kt b/vector/src/main/java/im/vector/riotredesign/core/services/PushSimulatorWorker.kt new file mode 100644 index 00000000..d3f93f32 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/services/PushSimulatorWorker.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.services + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters + +/** + * This class simulate push event when FCM is not working/disabled + */ +class PushSimulatorWorker(val context: Context, + workerParams: WorkerParameters) : Worker(context, workerParams) { + + override fun doWork(): Result { + // Simulate a Push + EventStreamServiceX.onSimulatedPushReceived(context) + + // Indicate whether the task finished successfully with the Result + return Result.success() + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/services/VectorService.kt b/vector/src/main/java/im/vector/riotredesign/core/services/VectorService.kt new file mode 100644 index 00000000..4b4c500d --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/services/VectorService.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.services + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import timber.log.Timber + +/** + * Parent class for all services + */ +abstract class VectorService : Service() { + + /** + * Tells if the service self destroyed. + */ + private var mIsSelfDestroyed = false + + override fun onCreate() { + super.onCreate() + + Timber.i("## onCreate() : $this") + } + + override fun onDestroy() { + Timber.i("## onDestroy() : $this") + + if (!mIsSelfDestroyed) { + Timber.w("## Destroy by the system : $this") + } + + super.onDestroy() + } + + protected fun myStopSelf() { + mIsSelfDestroyed = true + stopSelf() + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/RingtoneUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/RingtoneUtils.kt new file mode 100644 index 00000000..89c800d4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/utils/RingtoneUtils.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.utils + +import android.content.Context +import android.media.Ringtone +import android.media.RingtoneManager +import android.net.Uri +import android.preference.PreferenceManager +import androidx.core.content.edit +import im.vector.riotredesign.features.settings.PreferencesManager + +/** + * This file manages the sound ringtone for calls. + * It allows you to use the default Riot Ringtone, or the standard ringtone or set a different one from the available choices + * in Android. + */ + +/** + * Returns a Uri object that points to a specific Ringtone. + * + * If no Ringtone was explicitly set using Riot, it will return the Uri for the current system + * ringtone for calls. + * + * @return the [Uri] of the currently set [Ringtone] + * @see Ringtone + */ +fun getCallRingtoneUri(context: Context): Uri? { + val callRingtone: String? = PreferenceManager.getDefaultSharedPreferences(context) + .getString(PreferencesManager.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, null) + + callRingtone?.let { + return Uri.parse(it) + } + + return try { + // Use current system notification sound for incoming calls per default (note that it can return null) + RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE) + } catch (e: SecurityException) { + // Ignore for now + null + } +} + +/** + * Returns a Ringtone object that can then be played. + * + * If no Ringtone was explicitly set using Riot, it will return the current system ringtone + * for calls. + * + * @return the currently set [Ringtone] + * @see Ringtone + */ +fun getCallRingtone(context: Context): Ringtone? { + getCallRingtoneUri(context)?.let { + // Note that it can also return null + return RingtoneManager.getRingtone(context, it) + } + + return null +} + +/** + * Returns a String with the name of the current Ringtone. + * + * If no Ringtone was explicitly set using Riot, it will return the name of the current system + * ringtone for calls. + * + * @return the name of the currently set [Ringtone], or null + * @see Ringtone + */ +fun getCallRingtoneName(context: Context): String? { + return getCallRingtone(context)?.getTitle(context) +} + +/** + * Sets the selected ringtone for riot calls. + * + * @param ringtoneUri + * @see Ringtone + */ +fun setCallRingtoneUri(context: Context, ringtoneUri: Uri) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit { + putString(PreferencesManager.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, ringtoneUri.toString()) + } +} + +/** + * Set using Riot default ringtone + */ +fun useRiotDefaultRingtone(context: Context): Boolean { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(PreferencesManager.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, true) +} + +/** + * Ask if default Riot ringtone has to be used + */ +fun setUseRiotDefaultRingtone(context: Context, useRiotDefault: Boolean) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit { + putBoolean(PreferencesManager.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, useRiotDefault) + } +} + diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/SecretStoringUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/SecretStoringUtils.kt new file mode 100644 index 00000000..e139ee61 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/utils/SecretStoringUtils.kt @@ -0,0 +1,576 @@ +package im.vector.riotredesign.core.utils + +import android.content.Context +import android.os.Build +import android.security.KeyPairGeneratorSpec +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import androidx.annotation.RequiresApi +import java.io.* +import java.math.BigInteger +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.SecureRandom +import java.util.* +import javax.crypto.* +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.SecretKeySpec +import javax.security.auth.x500.X500Principal + + +/** + * Offers simple methods to securely store secrets in an Android Application. + * The encryption keys are randomly generated and securely managed by the key store, thus your secrets + * are safe. You only need to remember a key alias to perform encrypt/decrypt operations. + * + * Android M++ + * On android M+, the keystore can generates and store AES keys via API. But below API M this functionality + * is not available. + * + * Android [K-M[ + * For android >=KITKAT and Older androids + * For older androids as a fallback we generate an AES key from the alias using PBKDF2 with random salt. + * The salt and iv are stored with encrypted data. + * + * Sample usage: + * + * val secret = "The answer is 42" + * val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias", context) + * //This can be stored anywhere e.g. encoded in b64 and stored in preference for example + * + * //to get back the secret, just call + * val kDecripted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context) + * + * + * You can also just use this utility to store a secret key, and use any encryption algorthim that you want. + * + * Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you + * add a pin or change the schema); So you might and with a useless pile of bytes. + */ +object SecretStoringUtils { + + private const val ANDROID_KEY_STORE = "AndroidKeyStore" + private const val AES_MODE = "AES/GCM/NoPadding"; + private const val RSA_MODE = "RSA/ECB/PKCS1Padding" + + const val FORMAT_API_M: Byte = 0 + const val FORMAT_1: Byte = 1 + const val FORMAT_2: Byte = 2 + + val keyStore: KeyStore by lazy { + KeyStore.getInstance(ANDROID_KEY_STORE).apply { + load(null) + } + } + + private val secureRandom = SecureRandom() + + /** + * Encrypt the given secret using the android Keystore. + * On android >= M, will directly use the keystore to generate a symetric key + * On KitKat >= KitKat and = Build.VERSION_CODES.M) { + return encryptStringM(secret, keyAlias) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return encryptStringJ(secret, keyAlias, context) + } else { + return encryptForOldDevicesNotGood(secret, keyAlias) + } + } + + /** + * Decrypt a secret that was encrypted by #securelyStoreString() + */ + @Throws(Exception::class) + fun loadSecureSecret(encrypted: ByteArray, keyAlias: String, context: Context): String? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return decryptStringM(encrypted, keyAlias) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return decryptStringJ(encrypted, keyAlias, context) + } else { + return decryptForOldDevicesNotGood(encrypted, keyAlias) + } + } + + fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream, context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + saveSecureObjectM(keyAlias, output, any) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return saveSecureObjectK(keyAlias, output, any, context) + } else { + return saveSecureObjectOldNotGood(keyAlias, output, any) + } + } + + fun loadSecureSecret(inputStream: InputStream, keyAlias: String, context: Context): T? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return loadSecureObjectM(keyAlias, inputStream) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return loadSecureObjectK(keyAlias, inputStream, context) + } else { + return loadSecureObjectOldNotGood(keyAlias, inputStream) + } + } + + + @RequiresApi(Build.VERSION_CODES.M) + fun getOrGenerateSymmetricKeyForAlias(alias: String): SecretKey { + val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) + ?.secretKey + if (secretKeyEntry == null) { + //we generate it + val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") + val keyGenSpec = KeyGenParameterSpec.Builder(alias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(128) + .build() + generator.init(keyGenSpec) + return generator.generateKey() + } + return secretKeyEntry + } + + + /* + Symetric Key Generation is only available in M, so before M the idea is to: + - Generate a pair of RSA keys; + - Generate a random AES key; + - Encrypt the AES key using the RSA public key; + - Store the encrypted AES + Generate a key pair for encryption + */ + @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + fun getOrGenerateKeyPairForAlias(alias: String, context: Context): KeyStore.PrivateKeyEntry { + val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry) + + if (privateKeyEntry != null) return privateKeyEntry + + val start = Calendar.getInstance() + val end = Calendar.getInstance() + end.add(Calendar.YEAR, 30) + + val spec = KeyPairGeneratorSpec.Builder(context) + .setAlias(alias) + .setSubject(X500Principal("CN=$alias")) + .setSerialNumber(BigInteger.TEN) + //.setEncryptionRequired() requires that the phone as a pin/schema + .setStartDate(start.time) + .setEndDate(end.time) + .build() + KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE).run { + initialize(spec) + generateKeyPair() + } + return (keyStore.getEntry(alias, null) as KeyStore.PrivateKeyEntry) + + } + + + @RequiresApi(Build.VERSION_CODES.M) + fun encryptStringM(text: String, keyAlias: String): ByteArray? { + val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias) + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + val iv = cipher.iv + //we happen the iv to the final result + val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8)) + return formatMMake(iv, encryptedBytes) + } + + @RequiresApi(Build.VERSION_CODES.M) + fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String { + val (iv, encryptedText) = formatMExtract(ByteArrayInputStream(encryptedChunk)) + + val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias) + + val cipher = Cipher.getInstance(AES_MODE) + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + + return String(cipher.doFinal(encryptedText), Charsets.UTF_8) + } + + @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + fun encryptStringJ(text: String, keyAlias: String, context: Context): ByteArray? { + //we generate a random symetric key + val key = ByteArray(16) + secureRandom.nextBytes(key) + val sKey = SecretKeySpec(key, "AES") + + //we encrypt this key thanks to the key store + val encryptedKey = rsaEncrypt(keyAlias, key, context) + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, sKey) + val iv = cipher.iv + val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8)) + + return format1Make(encryptedKey, iv, encryptedBytes) + } + + fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray { + val salt = ByteArray(8) + secureRandom.nextBytes(salt) + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128) + val tmp = factory.generateSecret(spec) + val sKey = SecretKeySpec(tmp.encoded, "AES") + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, sKey) + val iv = cipher.iv + val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8)) + + return format2Make(salt, iv, encryptedBytes) + } + + fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? { + + val (salt, iv, encrypted) = format2Extract(ByteArrayInputStream(data)) + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128) + val tmp = factory.generateSecret(spec) + val sKey = SecretKeySpec(tmp.encoded, "AES") + + val cipher = Cipher.getInstance(AES_MODE) +// cipher.init(Cipher.ENCRYPT_MODE, sKey) +// val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8)) + + val specIV = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, sKey, specIV) + + return String(cipher.doFinal(encrypted), Charsets.UTF_8) + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + fun decryptStringJ(data: ByteArray, keyAlias: String, context: Context): String? { + + val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data)) + + //we need to decrypt the key + val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context) + val cipher = Cipher.getInstance(AES_MODE) + val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec) + + return String(cipher.doFinal(encrypted), Charsets.UTF_8) + + } + + + @RequiresApi(Build.VERSION_CODES.M) + @Throws(IOException::class) + fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) { + val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias) + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/) + val iv = cipher.iv + + val bos1 = ByteArrayOutputStream() + ObjectOutputStream(bos1).use { + it.writeObject(writeObject) + } + //Have to do it like that if i encapsulate the outputstream, the cipher could fail saying reuse IV + val doFinal = cipher.doFinal(bos1.toByteArray()) + output.write(FORMAT_API_M.toInt()) + output.write(iv.size) + output.write(iv) + output.write(doFinal) + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any, context: Context) { + //we generate a random symetric key + val key = ByteArray(16) + secureRandom.nextBytes(key) + val sKey = SecretKeySpec(key, "AES") + + //we encrypt this key thanks to the key store + val encryptedKey = rsaEncrypt(keyAlias, key, context) + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, sKey) + val iv = cipher.iv + + val bos1 = ByteArrayOutputStream() + val cos = CipherOutputStream(bos1, cipher) + ObjectOutputStream(cos).use { + it.writeObject(writeObject) + } + + output.write(FORMAT_1.toInt()) + output.write((encryptedKey.size and 0xFF00).shr(8)) + output.write(encryptedKey.size and 0x00FF) + output.write(encryptedKey) + output.write(iv.size) + output.write(iv) + output.write(bos1.toByteArray()) + } + + fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) { + val salt = ByteArray(8) + secureRandom.nextBytes(salt) + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val tmp = factory.generateSecret(PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)) + val secretKey = SecretKeySpec(tmp.encoded, "AES") + + + val cipher = Cipher.getInstance(AES_MODE) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + val iv = cipher.iv + + val bos1 = ByteArrayOutputStream() + ObjectOutputStream(bos1).use { + it.writeObject(writeObject) + } + //Have to do it like that if i encapsulate the outputstream, the cipher could fail saying reuse IV + val doFinal = cipher.doFinal(bos1.toByteArray()) + + output.write(FORMAT_2.toInt()) + output.write(salt.size) + output.write(salt) + output.write(iv.size) + output.write(iv) + output.write(doFinal) + } + +// @RequiresApi(Build.VERSION_CODES.M) +// @Throws(IOException::class) +// fun saveSecureObjectM(keyAlias: String, file: File, writeObject: Any) { +// FileOutputStream(file).use { +// saveSecureObjectM(keyAlias, it, writeObject) +// } +// } +// +// @RequiresApi(Build.VERSION_CODES.M) +// @Throws(IOException::class) +// fun loadSecureObjectM(keyAlias: String, file: File): T? { +// FileInputStream(file).use { +// return loadSecureObjectM(keyAlias, it) +// } +// } + + @RequiresApi(Build.VERSION_CODES.M) + @Throws(IOException::class) + fun loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? { + val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias) + + val format = inputStream.read() + assert(format.toByte() == FORMAT_API_M) + + val ivSize = inputStream.read() + val iv = ByteArray(ivSize) + inputStream.read(iv, 0, ivSize) + val cipher = Cipher.getInstance(AES_MODE) + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + + CipherInputStream(inputStream, cipher).use { cipherInputStream -> + ObjectInputStream(cipherInputStream).use { + val readObject = it.readObject() + return readObject as? T + } + } + + } + + @RequiresApi(Build.VERSION_CODES.KITKAT) + @Throws(IOException::class) + fun loadSecureObjectK(keyAlias: String, inputStream: InputStream, context: Context): T? { + + val (encryptedKey, iv, encrypted) = format1Extract(inputStream) + + //we need to decrypt the key + val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context) + val cipher = Cipher.getInstance(AES_MODE) + val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec) + + val encIS = ByteArrayInputStream(encrypted) + + CipherInputStream(encIS, cipher).use { cipherInputStream -> + ObjectInputStream(cipherInputStream).use { + val readObject = it.readObject() + return readObject as? T + } + } + } + + @Throws(Exception::class) + fun loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? { + + val (salt, iv, encrypted) = format2Extract(inputStream) + + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val tmp = factory.generateSecret(PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)) + val sKey = SecretKeySpec(tmp.encoded, "AES") + //we need to decrypt the key + + val cipher = Cipher.getInstance(AES_MODE) + val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, sKey, spec) + + val encIS = ByteArrayInputStream(encrypted) + + CipherInputStream(encIS, cipher).use { + ObjectInputStream(it).use { + val readObject = it.readObject() + return readObject as? T + } + } + } + + + @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + @Throws(Exception::class) + private fun rsaEncrypt(alias: String, secret: ByteArray, context: Context): ByteArray { + val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context) + // Encrypt the text + val inputCipher = Cipher.getInstance(RSA_MODE) + inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey) + + val outputStream = ByteArrayOutputStream() + val cipherOutputStream = CipherOutputStream(outputStream, inputCipher) + cipherOutputStream.write(secret) + cipherOutputStream.close() + + return outputStream.toByteArray() + } + + @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + @Throws(Exception::class) + private fun rsaDecrypt(alias: String, encrypted: InputStream, context: Context): ByteArray { + val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context) + val output = Cipher.getInstance(RSA_MODE) + output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey) + + val bos = ByteArrayOutputStream() + CipherInputStream(encrypted, output).use { + it.copyTo(bos) + } + + return bos.toByteArray() + } + + private fun formatMExtract(bis: InputStream): Pair { + val format = bis.read().toByte() + assert(format == FORMAT_API_M) + + val ivSize = bis.read() + val iv = ByteArray(ivSize) + bis.read(iv, 0, ivSize) + + + val bos = ByteArrayOutputStream() + var next = bis.read() + while (next != -1) { + bos.write(next) + next = bis.read() + } + val encrypted = bos.toByteArray() + return Pair(iv, encrypted) + } + + private fun formatMMake(iv: ByteArray, data: ByteArray): ByteArray { + val bos = ByteArrayOutputStream(2 + iv.size + data.size) + bos.write(FORMAT_API_M.toInt()) + bos.write(iv.size) + bos.write(iv) + bos.write(data) + return bos.toByteArray() + } + + private fun format1Extract(bis: InputStream): Triple { + + val format = bis.read() + assert(format.toByte() == FORMAT_1) + + val keySizeBig = bis.read() + val keySizeLow = bis.read() + val encryptedKeySize = keySizeBig.shl(8) + keySizeLow + val encryptedKey = ByteArray(encryptedKeySize) + bis.read(encryptedKey) + + val ivSize = bis.read() + val iv = ByteArray(ivSize) + bis.read(iv) + + val bos = ByteArrayOutputStream() + + var next = bis.read() + while (next != -1) { + bos.write(next) + next = bis.read() + } + val encrypted = bos.toByteArray() + return Triple(encryptedKey, iv, encrypted) + } + + private fun format1Make(encryptedKey: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray { + val bos = ByteArrayOutputStream(4 + encryptedKey.size + iv.size + encryptedBytes.size) + bos.write(FORMAT_1.toInt()) + bos.write((encryptedKey.size and 0xFF00).shr(8)) + bos.write(encryptedKey.size and 0x00FF) + bos.write(encryptedKey) + bos.write(iv.size) + bos.write(iv) + bos.write(encryptedBytes) + + return bos.toByteArray() + } + + private fun format2Make(salt: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray { + val bos = ByteArrayOutputStream(3 + salt.size + iv.size + encryptedBytes.size) + bos.write(FORMAT_2.toInt()) + bos.write(salt.size) + bos.write(salt) + bos.write(iv.size) + bos.write(iv) + bos.write(encryptedBytes) + + return bos.toByteArray() + } + + private fun format2Extract(bis: InputStream): Triple { + + val format = bis.read() + assert(format.toByte() == FORMAT_2) + + val saltSize = bis.read() + val salt = ByteArray(saltSize) + bis.read(salt) + + val ivSize = bis.read() + val iv = ByteArray(ivSize) + bis.read(iv) + + val bos = ByteArrayOutputStream() + + var next = bis.read() + while (next != -1) { + bos.write(next) + next = bis.read() + } + val encrypted = bos.toByteArray() + return Triple(salt, iv, encrypted) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/SystemUtils.kt index 5cf3ca49..d21f0d7c 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/utils/SystemUtils.kt @@ -26,6 +26,7 @@ import android.provider.Settings import android.widget.Toast import androidx.fragment.app.Fragment import im.vector.riotredesign.R +import im.vector.riotredesign.features.notifications.supportNotificationChannels import im.vector.riotredesign.features.settings.VectorLocale import timber.log.Timber import java.util.* @@ -124,10 +125,6 @@ fun startNotificationSettingsIntent(fragment: Fragment, requestCode: Int) { fragment.startActivityForResult(intent, requestCode) } -// TODO This comes from NotificationUtils -fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - - /** * Shows notification system settings for the given channel id. */ @@ -184,3 +181,8 @@ fun startImportTextFromFileIntent(fragment: Fragment, requestCode: Int) { fun Context.toast(resId: Int) { Toast.makeText(this, resId, Toast.LENGTH_SHORT).show() } + +// Not in KTX anymore +fun Context.toast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/badge/BadgeProxy.kt b/vector/src/main/java/im/vector/riotredesign/features/badge/BadgeProxy.kt new file mode 100644 index 00000000..dd529ff5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/badge/BadgeProxy.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.badge + +import android.content.Context +import android.os.Build +import im.vector.matrix.android.api.session.Session +import me.leolin.shortcutbadger.ShortcutBadger +import timber.log.Timber + +/** + * Manage application badge (displayed in the launcher) + */ +object BadgeProxy { + + /** + * Badge is now managed by notification channel, so no need to use compatibility library in recent versions + * + * @return true if library ShortcutBadger can be used + */ + private fun useShortcutBadger() = Build.VERSION.SDK_INT < Build.VERSION_CODES.O + + /** + * Update the application badge value. + * + * @param context the context + * @param badgeValue the new badge value + */ + fun updateBadgeCount(context: Context, badgeValue: Int) { + if (!useShortcutBadger()) { + return + } + + try { + ShortcutBadger.setBadge(context, badgeValue) + } catch (e: Exception) { + Timber.e(e, "## updateBadgeCount(): Exception Msg=" + e.message) + } + + } + + /** + * Refresh the badge count for specific configurations.

+ * The refresh is only effective if the device is: + * * offline * does not support FCM + * * FCM registration failed + *

Notifications rooms are parsed to track the notification count value. + * + * @param aSession session value + * @param aContext App context + */ + fun specificUpdateBadgeUnreadCount(aSession: Session?, aContext: Context?) { + if (!useShortcutBadger()) { + return + } + + /* TODO + val dataHandler: MXDataHandler + + // sanity check + if (null == aContext || null == aSession) { + Timber.w("## specificUpdateBadgeUnreadCount(): invalid input null values") + } else { + dataHandler = aSession.dataHandler + + if (dataHandler == null) { + Timber.w("## specificUpdateBadgeUnreadCount(): invalid DataHandler instance") + } else { + if (aSession.isAlive) { + var isRefreshRequired: Boolean + val pushManager = Matrix.getInstance(aContext)!!.pushManager + + // update the badge count if the device is offline, FCM is not supported or FCM registration failed + isRefreshRequired = !Matrix.getInstance(aContext)!!.isConnected + isRefreshRequired = isRefreshRequired or (null != pushManager && (!pushManager.useFcm() || !pushManager.hasRegistrationToken())) + + if (isRefreshRequired) { + updateBadgeCount(aContext, dataHandler) + } + } + } + } + */ + } + + /** + * Update the badge count value according to the rooms content. + * + * @param aContext App context + * @param aDataHandler data handler instance + */ + private fun updateBadgeCount(aSession: Session?, aContext: Context?) { + if (!useShortcutBadger()) { + return + } + + /* TODO + //sanity check + if (null == aContext || null == aDataHandler) { + Timber.w("## updateBadgeCount(): invalid input null values") + } else if (null == aDataHandler.store) { + Timber.w("## updateBadgeCount(): invalid store instance") + } else { + val roomCompleteList = ArrayList(aDataHandler.store.rooms) + var unreadRoomsCount = 0 + + for (room in roomCompleteList) { + if (room.notificationCount > 0) { + unreadRoomsCount++ + } + } + + // update the badge counter + Timber.d("## updateBadgeCount(): badge update count=$unreadRoomsCount") + updateBadgeCount(aContext, unreadRoomsCount) + } + */ + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt index c44b25ca..206a7e8b 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt @@ -37,6 +37,7 @@ import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment import im.vector.riotredesign.features.rageshake.BugReporter import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler +import im.vector.riotredesign.features.settings.VectorSettingsActivity import kotlinx.android.synthetic.main.activity_home.* import org.koin.android.ext.android.inject import org.koin.android.scope.ext.android.bindScope @@ -101,12 +102,18 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable { drawerToggle.syncState() } + override fun getMenuRes() = R.menu.home + override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { android.R.id.home -> { drawerLayout.openDrawer(GravityCompat.START) return true } + R.id.sliding_menu_settings -> { + startActivity(VectorSettingsActivity.getIntent(this, "TODO")) + return true + } } return true diff --git a/vector/src/main/java/im/vector/riotredesign/features/homeserver/ServerUrlsRepository.kt b/vector/src/main/java/im/vector/riotredesign/features/homeserver/ServerUrlsRepository.kt new file mode 100644 index 00000000..2893dded --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/homeserver/ServerUrlsRepository.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.homeserver + +import android.content.Context +import android.text.TextUtils +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import im.vector.riotredesign.R + +/** + * Object to store and retrieve home and identity server urls + */ +object ServerUrlsRepository { + + // Keys used to store default servers urls from the referrer + private const val DEFAULT_REFERRER_HOME_SERVER_URL_PREF = "default_referrer_home_server_url" + private const val DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF = "default_referrer_identity_server_url" + + // Keys used to store current home server url and identity url + const val HOME_SERVER_URL_PREF = "home_server_url" + const val IDENTITY_SERVER_URL_PREF = "identity_server_url" + + /** + * Save home and identity sever urls received by the Referrer receiver + */ + fun setDefaultUrlsFromReferrer(context: Context, homeServerUrl: String, identityServerUrl: String) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit { + if (!TextUtils.isEmpty(homeServerUrl)) { + putString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, homeServerUrl) + } + + if (!TextUtils.isEmpty(identityServerUrl)) { + putString(DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF, identityServerUrl) + } + } + } + + /** + * Save home and identity sever urls entered by the user. May be custom or default value + */ + fun saveServerUrls(context: Context, homeServerUrl: String, identityServerUrl: String) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit { + putString(HOME_SERVER_URL_PREF, homeServerUrl) + putString(IDENTITY_SERVER_URL_PREF, identityServerUrl) + } + } + + /** + * Return last used home server url, or the default one from referrer or the default one from resources + */ + fun getLastHomeServerUrl(context: Context): String { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + + return prefs.getString(HOME_SERVER_URL_PREF, + prefs.getString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, + getDefaultHomeServerUrl(context))) + } + + + /** + * Return last used identity server url, or the default one from referrer or the default one from resources + */ + fun getLastIdentityServerUrl(context: Context): String { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + + return prefs.getString(IDENTITY_SERVER_URL_PREF, + prefs.getString(DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF, + getDefaultIdentityServerUrl(context))) + } + + /** + * Return true if url is the default home server url form resources + */ + fun isDefaultHomeServerUrl(context: Context, url: String) = url == getDefaultHomeServerUrl(context) + + /** + * Return true if url is the default identity server url form resources + */ + fun isDefaultIdentityServerUrl(context: Context, url: String) = url == getDefaultIdentityServerUrl(context) + + /** + * Return default home server url from resources + */ + fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.default_hs_server_url) + + /** + * Return default identity server url from resources + */ + fun getDefaultIdentityServerUrl(context: Context): String = context.getString(R.string.default_identity_server_url) +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/IconLoader.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/IconLoader.kt new file mode 100644 index 00000000..61da3ea3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/IconLoader.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.notifications + +import android.content.Context +import android.os.Handler +import android.os.HandlerThread +import androidx.annotation.WorkerThread +import androidx.core.graphics.drawable.IconCompat +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DecodeFormat +import com.bumptech.glide.request.RequestOptions +import timber.log.Timber + +/** + * FIXME It works, but it does not refresh the notification, when it's already displayed + */ +class IconLoader(val context: Context, + val listener: IconLoaderListener) { + + /** + * Avatar Url -> Icon + */ + private val cache = HashMap() + + // URLs to load + private val toLoad = HashSet() + + // Black list of URLs (broken URL, etc.) + private val blacklist = HashSet() + + private var uiHandler = Handler() + + private val handlerThread: HandlerThread = HandlerThread("IconLoader", Thread.MIN_PRIORITY) + private var backgroundHandler: Handler + + init { + handlerThread.start() + backgroundHandler = Handler(handlerThread.looper) + } + + /** + * Get icon of a user. + * If already in cache, use it, else load it and call IconLoaderListener.onIconsLoaded() when ready + */ + fun getUserIcon(path: String?): IconCompat? { + if (path == null) { + return null + } + + synchronized(cache) { + if (cache[path] != null) { + return cache[path] + } + + // Add to the queue, if not blacklisted + if (!blacklist.contains(path)) { + if (toLoad.contains(path)) { + // Wait + } else { + toLoad.add(path) + + backgroundHandler.post { + loadUserIcon(path) + } + } + } + } + + return null + } + + @WorkerThread + private fun loadUserIcon(path: String) { + val iconCompat = path.let { + try { + Glide.with(context) + .asBitmap() + .load(path) + .apply(RequestOptions.circleCropTransform() + .format(DecodeFormat.PREFER_ARGB_8888)) + .submit() + .get() + } catch (e: Exception) { + Timber.e(e, "decodeFile failed") + null + }?.let { bitmap -> + IconCompat.createWithBitmap(bitmap) + } + } + + synchronized(cache) { + if (iconCompat == null) { + // Add to the blacklist + blacklist.add(path) + } else { + cache[path] = iconCompat + } + + toLoad.remove(path) + + if (toLoad.isEmpty()) { + uiHandler.post { + listener.onIconsLoaded() + } + } + } + } + + + interface IconLoaderListener { + fun onIconsLoaded() + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/InviteNotifiableEvent.kt new file mode 100644 index 00000000..34838de2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/InviteNotifiableEvent.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.notifications + +import androidx.core.app.NotificationCompat + + +data class InviteNotifiableEvent( + override var matrixID: String?, + override val eventId: String, + var roomId: String, + override var noisy: Boolean, + override val title: String, + override val description: String, + override val type: String?, + override val timestamp: Long, + override var soundName: String?, + override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { + + override var hasBeenDisplayed: Boolean = false + override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEvent.kt new file mode 100644 index 00000000..5e2cb667 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEvent.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.notifications + +import java.io.Serializable + +interface NotifiableEvent : Serializable { + var matrixID: String? + val eventId: String + var noisy: Boolean + val title: String + val description: String? + val type: String? + val timestamp: Long + //NotificationCompat.VISIBILITY_PUBLIC , VISIBILITY_PRIVATE , VISIBILITY_SECRET + var lockScreenVisibility: Int + // Compat: Only for android <7, for newer version the sound is defined in the channel + var soundName: String? + var hasBeenDisplayed: Boolean + //Used to know if event should be replaced with the one coming from eventstream + var isPushGatewayEvent: Boolean +} + diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEventResolver.kt new file mode 100644 index 00000000..83860d23 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableEventResolver.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.notifications + +import android.content.Context +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.riotredesign.core.preference.BingRule + +// TODO Remove +class RoomState { + +} + + +/** + * The notifiable event resolver is able to create a NotifiableEvent (view model for notifications) from an sdk Event. + * It is used as a bridge between the Event Thread and the NotificationDrawerManager. + * The NotifiableEventResolver is the only aware of session/store, the NotificationDrawerManager has no knowledge of that, + * this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk. + */ +class NotifiableEventResolver(val context: Context) { + + //private val eventDisplay = RiotEventDisplay(context) + + fun resolveEvent(event: Event, roomState: RoomState?, bingRule: BingRule?, session: Session): NotifiableEvent? { + // TODO + return null + /* + val store = session.dataHandler.store + if (store == null) { + Log.e("## NotifiableEventResolver, unable to get store") + //TODO notify somehow that something did fail? + return null + } + + when (event.type) { + EventType.MESSAGE -> { + return resolveMessageEvent(event, bingRule, session, store) + } + EventType.ENCRYPTED -> { + val messageEvent = resolveMessageEvent(event, bingRule, session, store) + messageEvent?.lockScreenVisibility = NotificationCompat.VISIBILITY_PRIVATE + return messageEvent + } + EventType.STATE_ROOM_MEMBER -> { + return resolveStateRoomEvent(event, bingRule, session, store) + } + else -> { + + //If the event can be displayed, display it as is + eventDisplay.getTextualDisplay(event, roomState)?.toString()?.let { body -> + return SimpleNotifiableEvent( + session.myUserId, + eventId = event.eventId, + noisy = bingRule?.notificationSound != null, + timestamp = event.originServerTs, + description = body, + soundName = bingRule?.notificationSound, + title = context.getString(R.string.notification_unknown_new_event), + type = event.type) + } + + //Unsupported event + Timber.w("NotifiableEventResolver Received an unsupported event matching a bing rule") + return null + } + } + */ + } + + /* + private fun resolveMessageEvent(event: Event, bingRule: BingRule?, session: MXSession, store: IMXStore): NotifiableEvent? { + //If we are here, that means that the event should be notified to the user, we check now how it should be presented (sound) + val soundName = bingRule?.notificationSound + val noisy = bingRule?.notificationSound != null + + //The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) + val room = store.getRoom(event.roomId /*roomID cannot be null (see Matrix SDK code)*/) + + if (room == null) { + Timber.e("## Unable to resolve room for eventId [${event.eventId}] and roomID [${event.roomId}]") + // Ok room is not known in store, but we can still display something + val body = eventDisplay.getTextualDisplay(event, null)?.toString() + ?: context.getString(R.string.notification_unknown_new_event) + val roomName = context.getString(R.string.notification_unknown_room_name) + val senderDisplayName = event.sender ?: "" + + val notifiableEvent = NotifiableMessageEvent( + eventId = event.eventId, + timestamp = event.originServerTs, + noisy = noisy, + senderName = senderDisplayName, + senderId = event.sender, + body = body, + roomId = event.roomId, + roomName = roomName) + + notifiableEvent.matrixID = session.myUserId + notifiableEvent.soundName = soundName + + return notifiableEvent + } else { + + val body = eventDisplay.getTextualDisplay(event, room.state)?.toString() + ?: context.getString(R.string.notification_unknown_new_event) + val roomName = room.getRoomDisplayName(context) + val senderDisplayName = room.state.getMemberName(event.sender) ?: event.sender ?: "" + + val notifiableEvent = NotifiableMessageEvent( + eventId = event.eventId, + timestamp = event.originServerTs, + noisy = noisy, + senderName = senderDisplayName, + senderId = event.sender, + body = body, + roomId = event.roomId, + roomName = roomName, + roomIsDirect = room.isDirect) + + notifiableEvent.matrixID = session.myUserId + notifiableEvent.soundName = soundName + + + val roomAvatarPath = session.mediaCache?.thumbnailCacheFile(room.avatarUrl, 50) + if (roomAvatarPath != null) { + notifiableEvent.roomAvatarPath = roomAvatarPath.path + } else { + // prepare for the next time + session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), room.avatarUrl, 50) + } + + room.state.getMember(event.sender)?.avatarUrl?.let { + val size = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) + val userAvatarUrlPath = session.mediaCache?.thumbnailCacheFile(it, size) + if (userAvatarUrlPath != null) { + notifiableEvent.senderAvatarPath = userAvatarUrlPath.path + } else { + // prepare for the next time + session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), it, size) + } + } + + return notifiableEvent + } + } + + private fun resolveStateRoomEvent(event: Event, bingRule: BingRule?, session: MXSession, store: IMXStore): NotifiableEvent? { + if (RoomMember.MEMBERSHIP_INVITE == event.contentAsJsonObject?.getAsJsonPrimitive("membership")?.asString) { + val room = store.getRoom(event.roomId /*roomID cannot be null (see Matrix SDK code)*/) + val body = eventDisplay.getTextualDisplay(event, room.state)?.toString() + ?: context.getString(R.string.notification_new_invitation) + return InviteNotifiableEvent( + session.myUserId, + eventId = event.eventId, + roomId = event.roomId, + timestamp = event.originServerTs, + noisy = bingRule?.notificationSound != null, + title = context.getString(R.string.notification_new_invitation), + description = body, + soundName = bingRule?.notificationSound, + type = event.type, + isPushGatewayEvent = false) + } else { + Timber.e("## unsupported notifiable event for eventId [${event.eventId}]") + if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { + Timber.e("## unsupported notifiable event for event [${event}]") + } + //TODO generic handling? + } + return null + } + */ +} + diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableMessageEvent.kt new file mode 100644 index 00000000..784aac2b --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotifiableMessageEvent.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.notifications + +import androidx.core.app.NotificationCompat +import im.vector.matrix.android.api.session.events.model.EventType + +data class NotifiableMessageEvent( + override val eventId: String, + override var noisy: Boolean, + override val timestamp: Long, + var senderName: String?, + var senderId: String?, + var body: String?, + var roomId: String, + var roomName: String?, + var roomIsDirect: Boolean = false +) : NotifiableEvent { + + + override var matrixID: String? = null + override var soundName: String? = null + override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC + override var hasBeenDisplayed: Boolean = false + + var roomAvatarPath: String? = null + var senderAvatarPath: String? = null + + override var isPushGatewayEvent: Boolean = false + + override val type: String + get() = EventType.MESSAGE + + override val description: String? + get() = body ?: "" + + override val title: String + get() = senderName ?: "" + + //This is used for >N notification, as the result of a smart reply + var outGoingMessage = false + var outGoingMessageFailed = false + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt new file mode 100644 index 00000000..d7f7aadd --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.notifications + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.core.app.RemoteInput +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.Room +import org.koin.standalone.KoinComponent +import org.koin.standalone.inject +import timber.log.Timber + +/** + * Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.) + */ +class NotificationBroadcastReceiver : BroadcastReceiver(), KoinComponent { + + private val notificationDrawerManager by inject() + + override fun onReceive(context: Context?, intent: Intent?) { + if (intent == null || context == null) return + + Timber.d("ReplyNotificationBroadcastReceiver received : $intent") + + when (intent.action) { + NotificationUtils.SMART_REPLY_ACTION -> + handleSmartReply(intent, context) + NotificationUtils.DISMISS_ROOM_NOTIF_ACTION -> + intent.getStringExtra(KEY_ROOM_ID)?.let { + notificationDrawerManager.clearMessageEventOfRoom(it) + } + NotificationUtils.DISMISS_SUMMARY_ACTION -> + notificationDrawerManager.clearAllEvents() + NotificationUtils.MARK_ROOM_READ_ACTION -> + intent.getStringExtra(KEY_ROOM_ID)?.let { + notificationDrawerManager.clearMessageEventOfRoom(it) + handleMarkAsRead(context, it) + } + } + } + + private fun handleMarkAsRead(context: Context, roomId: String) { + /* + TODO + Matrix.getInstance(context)?.defaultSession?.let { session -> + session.dataHandler + ?.getRoom(roomId) + ?.markAllAsRead(object : SimpleApiCallback() { + override fun onSuccess(void: Void?) { + // Ignore + } + }) + } + */ + } + + private fun handleSmartReply(intent: Intent, context: Context) { + /* + TODO + val message = getReplyMessage(intent) + val roomId = intent.getStringExtra(KEY_ROOM_ID) + + if (TextUtils.isEmpty(message) || TextUtils.isEmpty(roomId)) { + //ignore this event + //Can this happen? should we update notification? + return + } + val matrixId = intent.getStringExtra(EXTRA_MATRIX_ID) + Matrix.getInstance(context)?.getSession(matrixId)?.let { session -> + session.dataHandler?.getRoom(roomId)?.let { room -> + sendMatrixEvent(message!!, session, roomId!!, room, context) + } + } + */ + } + + private fun sendMatrixEvent(message: String, session: Session, roomId: String, room: Room, context: Context?) { + /* + TODO + + val mxMessage = Message() + mxMessage.msgtype = Message.MSGTYPE_TEXT + mxMessage.body = message + + val event = Event(mxMessage, session.credentials.userId, roomId) + room.storeOutgoingEvent(event) + room.sendEvent(event, object : ApiCallback { + override fun onSuccess(info: Void?) { + Timber.d("Send message : onSuccess ") + val notifiableMessageEvent = NotifiableMessageEvent( + event.eventId, + false, + System.currentTimeMillis(), + session.myUser?.displayname + ?: context?.getString(R.string.notification_sender_me), + session.myUserId, + message, + roomId, + room.getRoomDisplayName(context), + room.isDirect) + notifiableMessageEvent.outGoingMessage = true + VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent) + VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(null) + } + + override fun onNetworkError(e: Exception) { + Timber.d("Send message : onNetworkError " + e.message, e) + onSmartReplyFailed(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + Timber.d("Send message : onMatrixError " + e.message) + if (e is MXCryptoError) { + Toast.makeText(context, e.detailedErrorDescription, Toast.LENGTH_SHORT).show() + onSmartReplyFailed(e.detailedErrorDescription) + } else { + Toast.makeText(context, e.localizedMessage, Toast.LENGTH_SHORT).show() + onSmartReplyFailed(e.localizedMessage) + } + } + + override fun onUnexpectedError(e: Exception) { + Timber.e(e, "Send message : onUnexpectedError " + e.message) + onSmartReplyFailed(e.message) + } + + + fun onSmartReplyFailed(reason: String?) { + val notifiableMessageEvent = NotifiableMessageEvent( + event.eventId, + false, + System.currentTimeMillis(), + session.myUser?.displayname + ?: context?.getString(R.string.notification_sender_me), + session.myUserId, + message, + roomId, + room.getRoomDisplayName(context), + room.isDirect) + notifiableMessageEvent.outGoingMessage = true + notifiableMessageEvent.outGoingMessageFailed = true + + VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent) + VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(null) + } + }) + */ + } + + + private fun getReplyMessage(intent: Intent?): String? { + if (intent != null) { + val remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + return remoteInput.getCharSequence(KEY_TEXT_REPLY)?.toString() + } + } + return null + } + + companion object { + const val KEY_ROOM_ID = "roomID" + const val KEY_TEXT_REPLY = "key_text_reply" + const val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID" + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt new file mode 100644 index 00000000..1e0e3c44 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt @@ -0,0 +1,463 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.notifications + +import android.app.Notification +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.text.TextUtils +import androidx.core.app.NotificationCompat +import androidx.core.app.Person +import im.vector.matrix.android.api.session.Session +import im.vector.riotredesign.BuildConfig +import im.vector.riotredesign.R +import im.vector.riotredesign.core.utils.SecretStoringUtils +import timber.log.Timber +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream + +/** + * The NotificationDrawerManager receives notification events as they arrived (from event stream or fcm) and + * organise them in order to display them in the notification drawer. + * Events can be grouped into the same notification, old (already read) events can be removed to do some cleaning. + */ +class NotificationDrawerManager(val context: Context) { + + //The first time the notification drawer is refreshed, we force re-render of all notifications + private var firstTime = true + + private var eventList = loadEventInfo() + private var myUserDisplayName: String = "" + private var myUserAvatarUrl: String = "" + + private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) + + private var currentRoomId: String? = null + + private var iconLoader = IconLoader(context, + object : IconLoader.IconLoaderListener { + override fun onIconsLoaded() { + // Force refresh + refreshNotificationDrawer(null) + } + }) + + /** + * No multi session support for now + */ + private fun initWithSession(session: Session?) { + session?.let { + /* + myUserDisplayName = it.myUser?.displayname ?: it.myUserId + + // User Avatar + it.myUser?.avatarUrl?.let { avatarUrl -> + val userAvatarUrlPath = it.mediaCache?.thumbnailCacheFile(avatarUrl, avatarSize) + if (userAvatarUrlPath != null) { + myUserAvatarUrl = userAvatarUrlPath.path + } else { + // prepare for the next time + session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), avatarUrl, avatarSize) + } + } + */ + } + } + + /** + Should be called as soon as a new event is ready to be displayed. + The notification corresponding to this event will not be displayed until + #refreshNotificationDrawer() is called. + Events might be grouped and there might not be one notification per event! + */ + fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { + //If we support multi session, event list should be per userId + //Currently only manage single session + if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { + Timber.d("%%%%%%%% onNotifiableEventReceived $notifiableEvent") + } + synchronized(eventList) { + val existing = eventList.firstOrNull { it.eventId == notifiableEvent.eventId } + if (existing != null) { + if (existing.isPushGatewayEvent) { + //Use the event coming from the event stream as it may contains more info than + //the fcm one (like type/content/clear text) + // In this case the message has already been notified, and might have done some noise + // So we want the notification to be updated even if it has already been displayed + // But it should make no noise (e.g when an encrypted message from FCM should be + // update with clear text after a sync) + notifiableEvent.hasBeenDisplayed = false + notifiableEvent.noisy = false + eventList.remove(existing) + eventList.add(notifiableEvent) + + } else { + //keep the existing one, do not replace + } + } else { + eventList.add(notifiableEvent) + } + + } + } + + /** + Clear all known events and refresh the notification drawer + */ + fun clearAllEvents() { + synchronized(eventList) { + eventList.clear() + } + refreshNotificationDrawer(null) + } + + /** Clear all known message events for this room and refresh the notification drawer */ + fun clearMessageEventOfRoom(roomId: String?) { + Timber.d("clearMessageEventOfRoom $roomId") + + if (roomId != null) { + eventList.removeAll { e -> + if (e is NotifiableMessageEvent) { + return@removeAll e.roomId == roomId + } + return@removeAll false + } + NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID) + } + refreshNotificationDrawer(null) + } + + /** + Should be called when the application is currently opened and showing timeline for the given roomId. + Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room. + */ + fun setCurrentRoom(roomId: String?) { + var hasChanged: Boolean + synchronized(eventList) { + hasChanged = roomId != currentRoomId + currentRoomId = roomId + } + if (hasChanged) { + clearMessageEventOfRoom(roomId) + } + } + + fun homeActivityDidResume(matrixID: String?) { + synchronized(eventList) { + eventList.removeAll { e -> + return@removeAll e !is NotifiableMessageEvent //messages are cleared when entering room + } + } + } + + fun clearMemberShipNotificationForRoom(roomId: String) { + synchronized(eventList) { + eventList.removeAll { e -> + if (e is InviteNotifiableEvent) { + return@removeAll e.roomId == roomId + } + return@removeAll false + } + } + } + + + fun refreshNotificationDrawer(outdatedDetector: OutdatedEventDetector?) { + if (myUserDisplayName.isBlank()) { + // TODO + // initWithSession(Matrix.getInstance(context).defaultSession) + } + + if (myUserDisplayName.isBlank()) { + // Should not happen, but myUserDisplayName cannot be blank if used to create a Person + return + } + + synchronized(eventList) { + + Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER ") + //TMP code + var hasNewEvent = false + var summaryIsNoisy = false + val summaryInboxStyle = NotificationCompat.InboxStyle() + + //group events by room to create a single MessagingStyle notif + val roomIdToEventMap: MutableMap> = HashMap() + val simpleEvents: ArrayList = ArrayList() + val notifications: ArrayList = ArrayList() + + val eventIterator = eventList.listIterator() + while (eventIterator.hasNext()) { + val event = eventIterator.next() + if (event is NotifiableMessageEvent) { + val roomId = event.roomId + var roomEvents = roomIdToEventMap[roomId] + if (roomEvents == null) { + roomEvents = ArrayList() + roomIdToEventMap[roomId] = roomEvents + } + + if (shouldIgnoreMessageEventInRoom(roomId) || outdatedDetector?.isMessageOutdated(event) == true) { + //forget this event + eventIterator.remove() + } else { + roomEvents.add(event) + } + } else { + simpleEvents.add(event) + } + } + + + Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER ${roomIdToEventMap.size} room groups") + + var globalLastMessageTimestamp = 0L + + //events have been grouped + for ((roomId, events) in roomIdToEventMap) { + + if (events.isEmpty()) { + //Just clear this notification + Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId has no more events") + NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID) + continue + } + + val roomGroup = RoomEventGroupInfo(roomId) + roomGroup.hasNewEvent = false + roomGroup.shouldBing = false + roomGroup.isDirect = events[0].roomIsDirect + val roomName = events[0].roomName ?: events[0].senderName ?: "" + val style = NotificationCompat.MessagingStyle(Person.Builder() + .setName(myUserDisplayName) + .setIcon(iconLoader.getUserIcon(myUserAvatarUrl)) + .setKey(events[0].matrixID) + .build()) + roomGroup.roomDisplayName = roomName + + style.isGroupConversation = !roomGroup.isDirect + + if (!roomGroup.isDirect) { + style.conversationTitle = roomName + } + + val largeBitmap = getRoomBitmap(events) + + + for (event in events) { + //if all events in this room have already been displayed there is no need to update it + if (!event.hasBeenDisplayed) { + roomGroup.shouldBing = roomGroup.shouldBing || event.noisy + roomGroup.customSound = event.soundName + } + roomGroup.hasNewEvent = roomGroup.hasNewEvent || !event.hasBeenDisplayed + + val senderPerson = Person.Builder() + .setName(event.senderName) + .setIcon(iconLoader.getUserIcon(event.senderAvatarPath)) + .setKey(event.senderId) + .build() + + if (event.outGoingMessage && event.outGoingMessageFailed) { + style.addMessage(context.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) + roomGroup.hasSmartReplyError = true + } else { + style.addMessage(event.body, event.timestamp, senderPerson) + } + event.hasBeenDisplayed = true //we can consider it as displayed + + //It is possible that this event was previously shown as an 'anonymous' simple notif. + //And now it will be merged in a single MessageStyle notif, so we can clean to be sure + NotificationUtils.cancelNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID) + } + + try { + val summaryLine = context.resources.getQuantityString( + R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size) + summaryInboxStyle.addLine(summaryLine) + } catch (e: Throwable) { + //String not found or bad format + Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string") + summaryInboxStyle.addLine(roomName) + } + + if (firstTime || roomGroup.hasNewEvent) { + //Should update displayed notification + Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId need refresh") + val lastMessageTimestamp = events.last().timestamp + + if (globalLastMessageTimestamp < lastMessageTimestamp) { + globalLastMessageTimestamp = lastMessageTimestamp + } + + NotificationUtils.buildMessagesListNotification(context, style, roomGroup, largeBitmap, lastMessageTimestamp, myUserDisplayName) + ?.let { + //is there an id for this room? + notifications.add(it) + NotificationUtils.showNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID, it) + } + hasNewEvent = true + summaryIsNoisy = summaryIsNoisy || roomGroup.shouldBing + } else { + Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId is up to date") + } + } + + + //Handle simple events + for (event in simpleEvents) { + //We build a simple event + if (firstTime || !event.hasBeenDisplayed) { + NotificationUtils.buildSimpleEventNotification(context, event, null, myUserDisplayName)?.let { + notifications.add(it) + NotificationUtils.showNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID, it) + event.hasBeenDisplayed = true //we can consider it as displayed + hasNewEvent = true + summaryIsNoisy = summaryIsNoisy || event.noisy + summaryInboxStyle.addLine(event.description) + } + } + } + + + //======== Build summary notification ========= + //On Android 7.0 (API level 24) and higher, the system automatically builds a summary for + // your group using snippets of text from each notification. The user can expand this + // notification to see each separate notification. + // To support older versions, which cannot show a nested group of notifications, + // you must create an extra notification that acts as the summary. + // This appears as the only notification and the system hides all the others. + // So this summary should include a snippet from all the other notifications, + // which the user can tap to open your app. + // The behavior of the group summary may vary on some device types such as wearables. + // To ensure the best experience on all devices and versions, always include a group summary when you create a group + // https://developer.android.com/training/notify-user/group + + if (eventList.isEmpty()) { + NotificationUtils.cancelNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID) + } else { + val nbEvents = roomIdToEventMap.size + simpleEvents.size + val sumTitle = context.resources.getQuantityString( + R.plurals.notification_compat_summary_title, nbEvents, nbEvents) + summaryInboxStyle.setBigContentTitle(sumTitle) + NotificationUtils.buildSummaryListNotification( + context, + summaryInboxStyle, + sumTitle, + noisy = hasNewEvent && summaryIsNoisy, + lastMessageTimestamp = globalLastMessageTimestamp + )?.let { + NotificationUtils.showNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID, it) + } + + if (hasNewEvent && summaryIsNoisy) { + try { + // turn the screen on for 3 seconds + /* + TODO + if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) { + val pm = VectorApp.getInstance().getSystemService(Context.POWER_SERVICE) as PowerManager + val wl = pm.newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP, + NotificationDrawerManager::class.java.name) + wl.acquire(3000) + wl.release() + } + */ + } catch (e: Throwable) { + Timber.e(e, "## Failed to turn screen on") + } + + } + } + //notice that we can get bit out of sync with actual display but not a big issue + firstTime = false + } + } + + private fun getRoomBitmap(events: ArrayList): Bitmap? { + if (events.isEmpty()) return null + + //Use the last event (most recent?) + val roomAvatarPath = events[events.size - 1].roomAvatarPath + ?: events[events.size - 1].senderAvatarPath + if (!TextUtils.isEmpty(roomAvatarPath)) { + val options = BitmapFactory.Options() + options.inPreferredConfig = Bitmap.Config.ARGB_8888 + try { + return BitmapFactory.decodeFile(roomAvatarPath, options) + } catch (oom: OutOfMemoryError) { + Timber.e(oom, "decodeFile failed with an oom") + } + + } + return null + } + + private fun shouldIgnoreMessageEventInRoom(roomId: String?): Boolean { + return currentRoomId != null && roomId == currentRoomId + } + + + fun persistInfo() { + if (eventList.isEmpty()) { + deleteCachedRoomNotifications(context) + return + } + try { + val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) + if (!file.exists()) file.createNewFile() + FileOutputStream(file).use { + SecretStoringUtils.securelyStoreObject(eventList, "notificationMgr", it, this.context) + } + } catch (e: Throwable) { + Timber.e(e, "## Failed to save cached notification info") + } + } + + private fun loadEventInfo(): ArrayList { + try { + val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) + if (file.exists()) { + FileInputStream(file).use { + val events: ArrayList? = SecretStoringUtils.loadSecureSecret(it, "notificationMgr", this.context) + if (events != null) { + return ArrayList(events.mapNotNull { it as? NotifiableEvent }) + } + } + } + } catch (e: Throwable) { + Timber.e(e, "## Failed to load cached notification info") + } + return ArrayList() + } + + private fun deleteCachedRoomNotifications(context: Context) { + val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) + if (file.exists()) { + file.delete() + } + } + + companion object { + private const val SUMMARY_NOTIFICATION_ID = 0 + private const val ROOM_MESSAGES_NOTIFICATION_ID = 1 + private const val ROOM_EVENT_NOTIFICATION_ID = 2 + + private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache" + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationUtils.kt new file mode 100755 index 00000000..9596fdad --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationUtils.kt @@ -0,0 +1,721 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.notifications + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import android.text.TextUtils +import androidx.annotation.StringRes +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.RemoteInput +import androidx.core.app.TaskStackBuilder +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import im.vector.riotredesign.BuildConfig +import im.vector.riotredesign.R +import im.vector.riotredesign.core.utils.startNotificationChannelSettingsIntent +import im.vector.riotredesign.features.home.HomeActivity +import im.vector.riotredesign.features.settings.PreferencesManager +import timber.log.Timber +import java.util.* + + +fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + +/** + * Util class for creating notifications. + */ +object NotificationUtils { + + /* ========================================================================================== + * IDs for notifications + * ========================================================================================== */ + + /** + * Identifier of the foreground notification used to keep the application alive + * when it runs in background. + * This notification, which is not removable by the end user, displays what + * the application is doing while in background. + */ + const val NOTIFICATION_ID_FOREGROUND_SERVICE = 61 + + /* ========================================================================================== + * IDs for actions + * ========================================================================================== */ + + private const val JOIN_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.JOIN_ACTION" + private const val REJECT_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.REJECT_ACTION" + private const val QUICK_LAUNCH_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.QUICK_LAUNCH_ACTION" + const val MARK_ROOM_READ_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.MARK_ROOM_READ_ACTION" + const val SMART_REPLY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.SMART_REPLY_ACTION" + const val DISMISS_SUMMARY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_SUMMARY_ACTION" + const val DISMISS_ROOM_NOTIF_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_ROOM_NOTIF_ACTION" + private const val TAP_TO_VIEW_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.TAP_TO_VIEW_ACTION" + + /* ========================================================================================== + * IDs for channels + * ========================================================================================== */ + + // on devices >= android O, we need to define a channel for each notifications + private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID" + + private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID" + + private const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" + private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2" + + /* ========================================================================================== + * Channel names + * ========================================================================================== */ + + /** + * Create notification channels. + * + * @param context the context + */ + @TargetApi(Build.VERSION_CODES.O) + fun createNotificationChannels(context: Context) { + if (!supportNotificationChannels()) { + return + } + + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) + + //Migration - the noisy channel was deleted and recreated when sound preference was changed (id was DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE + // + currentTimeMillis). + //Now the sound can only be change directly in system settings, so for app upgrading we are deleting this former channel + //Starting from this version the channel will not be dynamic + for (channel in notificationManager.notificationChannels) { + val channelId = channel.id + val legacyBaseName = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE" + if (channelId.startsWith(legacyBaseName)) { + notificationManager.deleteNotificationChannel(channelId) + } + } + //Migration - Remove deprecated channels + for (channelId in listOf("DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID", "CALL_NOTIFICATION_CHANNEL_ID")) { + notificationManager.getNotificationChannel(channelId)?.let { + notificationManager.deleteNotificationChannel(channelId) + } + } + + /** + * Default notification importance: shows everywhere, makes noise, but does not visually + * intrude. + */ + notificationManager.createNotificationChannel(NotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID, + context.getString(R.string.notification_noisy_notifications), + NotificationManager.IMPORTANCE_DEFAULT) + .apply { + description = context.getString(R.string.notification_noisy_notifications) + enableVibration(true) + enableLights(true) + lightColor = accentColor + }) + + /** + * Low notification importance: shows everywhere, but is not intrusive. + */ + notificationManager.createNotificationChannel(NotificationChannel(SILENT_NOTIFICATION_CHANNEL_ID, + context.getString(R.string.notification_silent_notifications), + NotificationManager.IMPORTANCE_LOW) + .apply { + description = context.getString(R.string.notification_silent_notifications) + setSound(null, null) + enableLights(true) + lightColor = accentColor + }) + + notificationManager.createNotificationChannel(NotificationChannel(LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID, + context.getString(R.string.notification_listening_for_events), + NotificationManager.IMPORTANCE_MIN) + .apply { + description = context.getString(R.string.notification_listening_for_events) + setSound(null, null) + setShowBadge(false) + }) + + notificationManager.createNotificationChannel(NotificationChannel(CALL_NOTIFICATION_CHANNEL_ID, + context.getString(R.string.call), + NotificationManager.IMPORTANCE_HIGH) + .apply { + description = context.getString(R.string.call) + setSound(null, null) + enableLights(true) + lightColor = accentColor + }) + } + + /** + * Build a polling thread listener notification + * + * @param context Android context + * @param subTitleResId subtitle string resource Id of the notification + * @return the polling thread listener notification + */ + @SuppressLint("NewApi") + fun buildForegroundServiceNotification(context: Context, @StringRes subTitleResId: Int): Notification { + // build the pending intent go to the home screen if this is clicked. + val i = Intent(context, HomeActivity::class.java) + i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + val pi = PendingIntent.getActivity(context, 0, i, 0) + + val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) + + val builder = NotificationCompat.Builder(context, LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID) + .setContentTitle(context.getString(subTitleResId)) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setSmallIcon(R.drawable.logo_transparent) + .setProgress(0, 0, true) + .setColor(accentColor) + .setContentIntent(pi) + + // hide the notification from the status bar + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + builder.priority = NotificationCompat.PRIORITY_MIN + } + + val notification = builder.build() + + notification.flags = notification.flags or Notification.FLAG_NO_CLEAR + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + // some devices crash if this field is not set + // even if it is deprecated + + // setLatestEventInfo() is deprecated on Android M, so we try to use + // reflection at runtime, to avoid compiler error: "Cannot resolve method.." + try { + val deprecatedMethod = notification.javaClass + .getMethod("setLatestEventInfo", + Context::class.java, + CharSequence::class.java, + CharSequence::class.java, + PendingIntent::class.java) + deprecatedMethod.invoke(notification, context, context.getString(R.string.app_name), context.getString(subTitleResId), pi) + } catch (ex: Exception) { + Timber.e(ex, "## buildNotification(): Exception - setLatestEventInfo() Msg=" + ex.message) + } + + } + return notification + } + + /** + * Build an incoming call notification. + * This notification starts the VectorHomeActivity which is in charge of centralizing the incoming call flow. + * + * @param context the context. + * @param isVideo true if this is a video call, false for voice call + * @param roomName the room name in which the call is pending. + * @param matrixId the matrix id + * @param callId the call id. + * @return the call notification. + */ + @SuppressLint("NewApi") + fun buildIncomingCallNotification(context: Context, + isVideo: Boolean, + roomName: String, + matrixId: String, + callId: String): Notification { + val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) + + val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) + .setContentTitle(ensureTitleNotEmpty(context, roomName)) + .apply { + if (isVideo) { + setContentText(context.getString(R.string.incoming_video_call)) + } else { + setContentText(context.getString(R.string.incoming_voice_call)) + } + } + .setSmallIcon(R.drawable.incoming_call_notification_transparent) + .setCategory(NotificationCompat.CATEGORY_CALL) + .setLights(accentColor, 500, 500) + + //Compat: Display the incoming call notification on the lock screen + builder.priority = NotificationCompat.PRIORITY_MAX + + // clear the activity stack to home activity + val intent = Intent(context, HomeActivity::class.java) + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) + // TODO .putExtra(VectorHomeActivity.EXTRA_CALL_SESSION_ID, matrixId) + // TODO .putExtra(VectorHomeActivity.EXTRA_CALL_ID, callId) + + // Recreate the back stack + val stackBuilder = TaskStackBuilder.create(context) + .addParentStack(HomeActivity::class.java) + .addNextIntent(intent) + + + // android 4.3 issue + // use a generator for the private requestCode. + // When using 0, the intent is not created/launched when the user taps on the notification. + // + val pendingIntent = stackBuilder.getPendingIntent(Random().nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT) + + builder.setContentIntent(pendingIntent) + + return builder.build() + } + + /** + * Build a pending call notification + * + * @param context the context. + * @param isVideo true if this is a video call, false for voice call + * @param roomName the room name in which the call is pending. + * @param roomId the room Id + * @param matrixId the matrix id + * @param callId the call id. + * @return the call notification. + */ + @SuppressLint("NewApi") + fun buildPendingCallNotification(context: Context, + isVideo: Boolean, + roomName: String, + roomId: String, + matrixId: String, + callId: String): Notification { + + val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) + .setContentTitle(ensureTitleNotEmpty(context, roomName)) + .apply { + if (isVideo) { + setContentText(context.getString(R.string.video_call_in_progress)) + } else { + setContentText(context.getString(R.string.call_in_progress)) + } + } + .setSmallIcon(R.drawable.incoming_call_notification_transparent) + .setCategory(NotificationCompat.CATEGORY_CALL) + + // Display the pending call notification on the lock screen + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + builder.priority = NotificationCompat.PRIORITY_MAX + } + + /* TODO + // Build the pending intent for when the notification is clicked + val roomIntent = Intent(context, VectorRoomActivity::class.java) + .putExtra(VectorRoomActivity.EXTRA_ROOM_ID, roomId) + .putExtra(VectorRoomActivity.EXTRA_MATRIX_ID, matrixId) + .putExtra(VectorRoomActivity.EXTRA_START_CALL_ID, callId) + + // Recreate the back stack + val stackBuilder = TaskStackBuilder.create(context) + .addParentStack(VectorRoomActivity::class.java) + .addNextIntent(roomIntent) + + // android 4.3 issue + // use a generator for the private requestCode. + // When using 0, the intent is not created/launched when the user taps on the notification. + // + val pendingIntent = stackBuilder.getPendingIntent(Random().nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT) + + builder.setContentIntent(pendingIntent) + */ + + return builder.build() + } + + /** + * Build a temporary (because service will be stopped just after) notification for the CallService, when a call is ended + */ + fun buildCallEndedNotification(context: Context): Notification { + return NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) + .setContentTitle(context.getString(R.string.call_ended)) + .setSmallIcon(R.drawable.ic_material_call_end_grey) + .setCategory(NotificationCompat.CATEGORY_CALL) + .build() + } + + /** + * Build a notification for a Room + */ + fun buildMessagesListNotification(context: Context, + messageStyle: NotificationCompat.MessagingStyle, + roomInfo: RoomEventGroupInfo, + largeIcon: Bitmap?, + lastMessageTimestamp: Long, + senderDisplayNameForReplyCompat: String?): Notification? { + + val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) + // Build the pending intent for when the notification is clicked + val openRoomIntent = buildOpenRoomIntent(context, roomInfo.roomId) + val smallIcon = if (roomInfo.shouldBing) R.drawable.icon_notif_important else R.drawable.logo_transparent + + val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID + return NotificationCompat.Builder(context, channelID) + .setWhen(lastMessageTimestamp) + // MESSAGING_STYLE sets title and content for API 16 and above devices. + .setStyle(messageStyle) + + // A category allows groups of notifications to be ranked and filtered – per user or system settings. + // For example, alarm notifications should display before promo notifications, or message from known contact + // that can be displayed in not disturb mode if white listed (the later will need compat28.x) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + + // Title for API < 16 devices. + .setContentTitle(roomInfo.roomDisplayName) + // Content for API < 16 devices. + .setContentText(context.getString(R.string.notification_new_messages)) + + // Number of new notifications for API <24 (M and below) devices. + .setSubText(context + .resources + .getQuantityString(R.plurals.room_new_messages_notification, messageStyle.messages.size, messageStyle.messages.size) + ) + + // Auto-bundling is enabled for 4 or more notifications on API 24+ (N+) + // devices and all Wear devices. But we want a custom grouping, so we specify the groupID + // TODO Group should be current user display name + .setGroup(context.getString(R.string.app_name)) + + //In order to avoid notification making sound twice (due to the summary notification) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + + .setSmallIcon(smallIcon) + + // Set primary color (important for Wear 2.0 Notifications). + .setColor(accentColor) + + // Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for + // 'importance' which is set in the NotificationChannel. The integers representing + // 'priority' are different from 'importance', so make sure you don't mix them. + .apply { + priority = NotificationCompat.PRIORITY_DEFAULT + if (roomInfo.shouldBing) { + //Compat + PreferencesManager.getNotificationRingTone(context)?.let { + setSound(it) + } + setLights(accentColor, 500, 500) + } else { + priority = NotificationCompat.PRIORITY_LOW + } + + //Add actions and notification intents + // Mark room as read + val markRoomReadIntent = Intent(context, NotificationBroadcastReceiver::class.java) + markRoomReadIntent.action = MARK_ROOM_READ_ACTION + markRoomReadIntent.data = Uri.parse("foobar://${roomInfo.roomId}") + markRoomReadIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId) + val markRoomReadPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), markRoomReadIntent, + PendingIntent.FLAG_UPDATE_CURRENT) + + addAction(NotificationCompat.Action( + R.drawable.ic_material_done_all_white, + context.getString(R.string.action_mark_room_read), + markRoomReadPendingIntent)) + + // Quick reply + if (!roomInfo.hasSmartReplyError) { + buildQuickReplyIntent(context, roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent -> + val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY) + .setLabel(context.getString(R.string.action_quick_reply)) + .build() + NotificationCompat.Action.Builder(R.drawable.vector_notification_quick_reply, + context.getString(R.string.action_quick_reply), replyPendingIntent) + .addRemoteInput(remoteInput) + .build()?.let { + addAction(it) + } + } + } + + if (openRoomIntent != null) { + setContentIntent(openRoomIntent) + } + + if (largeIcon != null) { + setLargeIcon(largeIcon) + } + + val intent = Intent(context, NotificationBroadcastReceiver::class.java) + intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId) + intent.action = DISMISS_ROOM_NOTIF_ACTION + val pendingIntent = PendingIntent.getBroadcast(context.applicationContext, + System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT) + setDeleteIntent(pendingIntent) + } + .build() + } + + + fun buildSimpleEventNotification(context: Context, simpleNotifiableEvent: NotifiableEvent, largeIcon: Bitmap?, matrixId: String): Notification? { + val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) + // Build the pending intent for when the notification is clicked + val smallIcon = if (simpleNotifiableEvent.noisy) R.drawable.icon_notif_important else R.drawable.logo_transparent + + val channelID = if (simpleNotifiableEvent.noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID + + return NotificationCompat.Builder(context, channelID) + .setContentTitle(context.getString(R.string.app_name)) + .setContentText(simpleNotifiableEvent.description) + .setGroup(context.getString(R.string.app_name)) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setSmallIcon(smallIcon) + .setColor(accentColor) + .apply { + if (simpleNotifiableEvent is InviteNotifiableEvent) { + /* + TODO + val roomId = simpleNotifiableEvent.roomId + // offer to type a quick reject button + val rejectIntent = JoinRoomActivity.getRejectRoomIntent(context, roomId, matrixId) + + // the action must be unique else the parameters are ignored + rejectIntent.action = REJECT_ACTION + rejectIntent.data = Uri.parse("foobar://$roomId&$matrixId") + addAction( + R.drawable.vector_notification_reject_invitation, + context.getString(R.string.reject), + PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), rejectIntent, 0)) + + // offer to type a quick accept button + val joinIntent = JoinRoomActivity.getJoinRoomIntent(context, roomId, matrixId) + + // the action must be unique else the parameters are ignored + joinIntent.action = JOIN_ACTION + joinIntent.data = Uri.parse("foobar://$roomId&$matrixId") + addAction( + R.drawable.vector_notification_accept_invitation, + context.getString(R.string.join), + PendingIntent.getActivity(context, 0, joinIntent, 0)) + */ + } else { + setAutoCancel(true) + } + + val contentIntent = Intent(context, HomeActivity::class.java) + contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + //pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that + contentIntent.data = Uri.parse("foobar://" + simpleNotifiableEvent.eventId) + setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, 0)) + + if (largeIcon != null) { + setLargeIcon(largeIcon) + } + + if (simpleNotifiableEvent.noisy) { + //Compat + priority = NotificationCompat.PRIORITY_DEFAULT + PreferencesManager.getNotificationRingTone(context)?.let { + setSound(it) + } + setLights(accentColor, 500, 500) + } else { + priority = NotificationCompat.PRIORITY_LOW + } + setAutoCancel(true) + } + .build() + } + + private fun buildOpenRoomIntent(context: Context, roomId: String): PendingIntent? { + // TODO + return null + /* + val roomIntentTap = Intent(context, VectorRoomActivity::class.java) + roomIntentTap.putExtra(VectorRoomActivity.EXTRA_ROOM_ID, roomId) + roomIntentTap.action = TAP_TO_VIEW_ACTION + //pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that + roomIntentTap.data = Uri.parse("foobar://openRoom?$roomId") + + // Recreate the back stack + return TaskStackBuilder.create(context) + .addNextIntentWithParentStack(Intent(context, VectorHomeActivity::class.java)) + .addNextIntent(roomIntentTap) + .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) + */ + } + + private fun buildOpenHomePendingIntentForSummary(context: Context): PendingIntent { + val intent = Intent(context, HomeActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + // TODO intent.putExtra(VectorHomeActivity.EXTRA_CLEAR_EXISTING_NOTIFICATION, true) + intent.data = Uri.parse("foobar://tapSummary") + return PendingIntent.getActivity(context, Random().nextInt(1000), intent, PendingIntent.FLAG_UPDATE_CURRENT) + } + + /* + Direct reply is new in Android N, and Android already handles the UI, so the right pending intent + here will ideally be a Service/IntentService (for a long running background task) or a BroadcastReceiver, + which runs on the UI thread. It also works without unlocking, making the process really fluid for the user. + However, for Android devices running Marshmallow and below (API level 23 and below), + it will be more appropriate to use an activity. Since you have to provide your own UI. + */ + private fun buildQuickReplyIntent(context: Context, roomId: String, senderName: String?): PendingIntent? { + val intent: Intent + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + intent = Intent(context, NotificationBroadcastReceiver::class.java) + intent.action = SMART_REPLY_ACTION + intent.data = Uri.parse("foobar://$roomId") + intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) + return PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), intent, + PendingIntent.FLAG_UPDATE_CURRENT) + } else { + /* + TODO + if (!LockScreenActivity.isDisplayingALockScreenActivity()) { + // start your activity for Android M and below + val quickReplyIntent = Intent(context, LockScreenActivity::class.java) + quickReplyIntent.putExtra(LockScreenActivity.EXTRA_ROOM_ID, roomId) + quickReplyIntent.putExtra(LockScreenActivity.EXTRA_SENDER_NAME, senderName ?: "") + + // the action must be unique else the parameters are ignored + quickReplyIntent.action = QUICK_LAUNCH_ACTION + quickReplyIntent.data = Uri.parse("foobar://$roomId") + return PendingIntent.getActivity(context, 0, quickReplyIntent, 0) + } + */ + } + return null + } + + //// Number of new notifications for API <24 (M and below) devices. + /** + * Build the summary notification + */ + fun buildSummaryListNotification(context: Context, + style: NotificationCompat.Style, + compatSummary: String, + noisy: Boolean, + lastMessageTimestamp: Long): Notification? { + val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) + val smallIcon = if (noisy) R.drawable.icon_notif_important else R.drawable.logo_transparent + + return NotificationCompat.Builder(context, if (noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID) + // used in compat < N, after summary is built based on child notifications + .setWhen(lastMessageTimestamp) + .setStyle(style) + .setContentTitle(context.getString(R.string.app_name)) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .setSmallIcon(smallIcon) + //set content text to support devices running API level < 24 + .setContentText(compatSummary) + .setGroup(context.getString(R.string.app_name)) + //set this notification as the summary for the group + .setGroupSummary(true) + .setColor(accentColor) + .apply { + if (noisy) { + //Compat + priority = NotificationCompat.PRIORITY_DEFAULT + PreferencesManager.getNotificationRingTone(context)?.let { + setSound(it) + } + setLights(accentColor, 500, 500) + } else { + //compat + priority = NotificationCompat.PRIORITY_LOW + } + } + .setContentIntent(buildOpenHomePendingIntentForSummary(context)) + .setDeleteIntent(getDismissSummaryPendingIntent(context)) + .build() + + } + + private fun getDismissSummaryPendingIntent(context: Context): PendingIntent { + val intent = Intent(context, NotificationBroadcastReceiver::class.java) + intent.action = DISMISS_SUMMARY_ACTION + intent.data = Uri.parse("foobar://deleteSummary") + return PendingIntent.getBroadcast(context.applicationContext, + 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } + + fun showNotificationMessage(context: Context, tag: String?, id: Int, notification: Notification) { + with(NotificationManagerCompat.from(context)) { + notify(tag, id, notification) + } + } + + fun cancelNotificationMessage(context: Context, tag: String?, id: Int) { + NotificationManagerCompat.from(context) + .cancel(tag, id) + } + + /** + * Cancel the foreground notification service + */ + fun cancelNotificationForegroundService(context: Context) { + NotificationManagerCompat.from(context) + .cancel(NOTIFICATION_ID_FOREGROUND_SERVICE) + } + + /** + * Cancel all the notification + */ + fun cancelAllNotifications(context: Context) { + // Keep this try catch (reported by GA) + try { + NotificationManagerCompat.from(context) + .cancelAll() + } catch (e: Exception) { + Timber.e(e, "## cancelAllNotifications() failed " + e.message) + } + } + + /** + * Return true it the user has enabled the do not disturb mode + */ + fun isDoNotDisturbModeOn(context: Context): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return false + } + + // We cannot use NotificationManagerCompat here. + val setting = (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).currentInterruptionFilter + + return setting == NotificationManager.INTERRUPTION_FILTER_NONE + || setting == NotificationManager.INTERRUPTION_FILTER_ALARMS + } + + private fun ensureTitleNotEmpty(context: Context, title: String?): CharSequence { + if (TextUtils.isEmpty(title)) { + return context.getString(R.string.app_name) + } + + return title!! + } + + fun openSystemSettingsForSilentCategory(fragment: Fragment) { + startNotificationChannelSettingsIntent(fragment, SILENT_NOTIFICATION_CHANNEL_ID) + } + + fun openSystemSettingsForNoisyCategory(fragment: Fragment) { + startNotificationChannelSettingsIntent(fragment, NOISY_NOTIFICATION_CHANNEL_ID) + } + + fun openSystemSettingsForCallCategory(fragment: Fragment) { + startNotificationChannelSettingsIntent(fragment, CALL_NOTIFICATION_CHANNEL_ID) + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/OutdatedEventDetector.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/OutdatedEventDetector.kt new file mode 100644 index 00000000..b1fc55c8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/OutdatedEventDetector.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.notifications + +import android.content.Context + +class OutdatedEventDetector(val context: Context) { + + /** + * Returns true if the given event is outdated. + * Used to clean up notifications if a displayed message has been read on an + * other device. + */ + fun isMessageOutdated(notifiableEvent: NotifiableEvent): Boolean { + if (notifiableEvent is NotifiableMessageEvent) { + val eventID = notifiableEvent.eventId + val roomID = notifiableEvent.roomId + /* + TODO + Matrix.getMXSession(context.applicationContext, notifiableEvent.matrixID)?.let { session -> + //find the room + if (session.isAlive) { + session.dataHandler.getRoom(roomID)?.let { room -> + if (room.isEventRead(eventID)) { + Timber.d("Notifiable Event $eventID is read, and should be removed") + return true + } + } + } + } + */ + } + return false + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/RoomEventGroupInfo.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/RoomEventGroupInfo.kt new file mode 100644 index 00000000..e1c4e582 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/RoomEventGroupInfo.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.notifications + +/** + * Data class to hold information about a group of notifications for a room + */ +data class RoomEventGroupInfo( + val roomId: String +) { + var roomDisplayName: String = "" + var roomAvatarPath: String? = null + //An event in the list has not yet been display + var hasNewEvent: Boolean = false + //true if at least one on the not yet displayed event is noisy + var shouldBing: Boolean = false + var customSound: String? = null + var hasSmartReplyError = false + var isDirect = false +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/SimpleNotifiableEvent.kt new file mode 100644 index 00000000..b0226ca3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/SimpleNotifiableEvent.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.notifications + +import androidx.core.app.NotificationCompat + +data class SimpleNotifiableEvent( + override var matrixID: String?, + override val eventId: String, + override var noisy: Boolean, + override val title: String, + override val description: String, + override val type: String?, + override val timestamp: Long, + override var soundName: String?, + override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { + + override var hasBeenDisplayed: Boolean = false + override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC + +} + diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/PreferencesManager.java b/vector/src/main/java/im/vector/riotredesign/features/settings/PreferencesManager.java new file mode 100755 index 00000000..845af353 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/PreferencesManager.java @@ -0,0 +1,861 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.settings; + +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.media.RingtoneManager; +import android.net.Uri; +import android.provider.MediaStore; +import android.text.TextUtils; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; +import im.vector.riotredesign.R; +import im.vector.riotredesign.features.homeserver.ServerUrlsRepository; +import im.vector.riotredesign.features.themes.ThemeUtils; +import timber.log.Timber; + +public class PreferencesManager { + + public static final String VERSION_BUILD = "VERSION_BUILD"; + + public static final String SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY = "SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY_2"; + public static final String SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY = "SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY"; + public static final String SETTINGS_VERSION_PREFERENCE_KEY = "SETTINGS_VERSION_PREFERENCE_KEY"; + public static final String SETTINGS_OLM_VERSION_PREFERENCE_KEY = "SETTINGS_OLM_VERSION_PREFERENCE_KEY"; + public static final String SETTINGS_LOGGED_IN_PREFERENCE_KEY = "SETTINGS_LOGGED_IN_PREFERENCE_KEY"; + public static final String SETTINGS_HOME_SERVER_PREFERENCE_KEY = "SETTINGS_HOME_SERVER_PREFERENCE_KEY"; + public static final String SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY"; + public static final String SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY = "SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY"; + public static final String SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY = "SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY"; + public static final String SETTINGS_NOTIFICATION_PRIVACY_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_PRIVACY_PREFERENCE_KEY"; + public static final String SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY"; + public static final String SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY"; + public static final String SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY = "SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY"; + public static final String SETTINGS_COPYRIGHT_PREFERENCE_KEY = "SETTINGS_COPYRIGHT_PREFERENCE_KEY"; + public static final String SETTINGS_CLEAR_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_CACHE_PREFERENCE_KEY"; + public static final String SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY"; + public static final String SETTINGS_USER_SETTINGS_PREFERENCE_KEY = "SETTINGS_USER_SETTINGS_PREFERENCE_KEY"; + public static final String SETTINGS_CONTACT_PREFERENCE_KEYS = "SETTINGS_CONTACT_PREFERENCE_KEYS"; + public static final String SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY"; + public static final String SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY"; + public static final String SETTINGS_IGNORED_USERS_PREFERENCE_KEY = "SETTINGS_IGNORED_USERS_PREFERENCE_KEY"; + public static final String SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY = "SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY"; + public static final String SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY"; + public static final String SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY"; + public static final String SETTINGS_LABS_PREFERENCE_KEY = "SETTINGS_LABS_PREFERENCE_KEY"; + public static final String SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY"; + public static final String SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY"; + public static final String SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"; + public static final String SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY"; + public static final String SETTINGS_DEVICES_LIST_PREFERENCE_KEY = "SETTINGS_DEVICES_LIST_PREFERENCE_KEY"; + public static final String SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY = "SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY"; + public static final String SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY"; + public static final String SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY + = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY"; + public static final String SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY"; + public static final String SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY"; + public static final String SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY"; + public static final String SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY"; + public static final String SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY"; + public static final String SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY"; + + public static final String SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY"; + + // user + public static final String SETTINGS_DISPLAY_NAME_PREFERENCE_KEY = "SETTINGS_DISPLAY_NAME_PREFERENCE_KEY"; + public static final String SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY = "SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY"; + + // contacts + public static final String SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY = "SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY"; + + // interface + public static final String SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY = "SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY"; + public static final String SETTINGS_INTERFACE_TEXT_SIZE_KEY = "SETTINGS_INTERFACE_TEXT_SIZE_KEY"; + public static final String SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY"; + private static final String SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY"; + private static final String SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY"; + private static final String SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY = "SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY"; + private static final String SETTINGS_12_24_TIMESTAMPS_KEY = "SETTINGS_12_24_TIMESTAMPS_KEY"; + private static final String SETTINGS_SHOW_READ_RECEIPTS_KEY = "SETTINGS_SHOW_READ_RECEIPTS_KEY"; + private static final String SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY = "SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY"; + private static final String SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY = "SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY"; + private static final String SETTINGS_VIBRATE_ON_MENTION_KEY = "SETTINGS_VIBRATE_ON_MENTION_KEY"; + private static final String SETTINGS_SEND_MESSAGE_WITH_ENTER = "SETTINGS_SEND_MESSAGE_WITH_ENTER"; + + // home + private static final String SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY = "SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY"; + private static final String SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY = "SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY"; + + // flair + public static final String SETTINGS_GROUPS_FLAIR_KEY = "SETTINGS_GROUPS_FLAIR_KEY"; + + // notifications + public static final String SETTINGS_NOTIFICATIONS_KEY = "SETTINGS_NOTIFICATIONS_KEY"; + public static final String SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY = "SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY"; + public static final String SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY = "SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY"; + public static final String SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY = "SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY"; + public static final String SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY"; + public static final String SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY"; + public static final String SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY"; + public static final String SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY"; + public static final String SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY"; + public static final String SETTINGS_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY = "SETTINGS_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY_2"; + public static final String SETTINGS_CONTAINING_MY_USER_NAME_PREFERENCE_KEY = "SETTINGS_CONTAINING_MY_USER_NAME_PREFERENCE_KEY_2"; + public static final String SETTINGS_MESSAGES_IN_ONE_TO_ONE_PREFERENCE_KEY = "SETTINGS_MESSAGES_IN_ONE_TO_ONE_PREFERENCE_KEY_2"; + public static final String SETTINGS_MESSAGES_IN_GROUP_CHAT_PREFERENCE_KEY = "SETTINGS_MESSAGES_IN_GROUP_CHAT_PREFERENCE_KEY_2"; + public static final String SETTINGS_INVITED_TO_ROOM_PREFERENCE_KEY = "SETTINGS_INVITED_TO_ROOM_PREFERENCE_KEY_2"; + public static final String SETTINGS_CALL_INVITATIONS_PREFERENCE_KEY = "SETTINGS_CALL_INVITATIONS_PREFERENCE_KEY_2"; + + // media + private static final String SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY = "SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY"; + private static final String SETTINGS_DEFAULT_MEDIA_SOURCE_KEY = "SETTINGS_DEFAULT_MEDIA_SOURCE_KEY"; + private static final String SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY = "SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY"; + private static final String SETTINGS_PLAY_SHUTTER_SOUND_KEY = "SETTINGS_PLAY_SHUTTER_SOUND_KEY"; + + // background sync + public static final String SETTINGS_START_ON_BOOT_PREFERENCE_KEY = "SETTINGS_START_ON_BOOT_PREFERENCE_KEY"; + public static final String SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY"; + public static final String SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY = "SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY"; + public static final String SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY = "SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY"; + + // Calls + public static final String SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY = "SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY"; + public static final String SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY = "SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY"; + + // labs + public static final String SETTINGS_LAZY_LOADING_PREFERENCE_KEY = "SETTINGS_LAZY_LOADING_PREFERENCE_KEY"; + public static final String SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY = "SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY"; + public static final String SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY = "SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY"; + private static final String SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY = "SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY"; + private static final String SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY = "SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY"; + private static final String SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY = "SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY"; + + // analytics + public static final String SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY"; + public static final String SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"; + + // other + public static final String SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY"; + private static final String SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY"; + private static final String DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY = "DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY"; + private static final String DID_MIGRATE_TO_NOTIFICATION_REWORK = "DID_MIGRATE_TO_NOTIFICATION_REWORK"; + private static final String DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY = "DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY"; + public static final String SETTINGS_DEACTIVATE_ACCOUNT_KEY = "SETTINGS_DEACTIVATE_ACCOUNT_KEY"; + private static final String SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY"; + + private static final int MEDIA_SAVING_3_DAYS = 0; + private static final int MEDIA_SAVING_1_WEEK = 1; + private static final int MEDIA_SAVING_1_MONTH = 2; + private static final int MEDIA_SAVING_FOREVER = 3; + + // some preferences keys must be kept after a logout + private static final List mKeysToKeepAfterLogout = Arrays.asList( + SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY, + SETTINGS_DEFAULT_MEDIA_SOURCE_KEY, + SETTINGS_PLAY_SHUTTER_SOUND_KEY, + + SETTINGS_SEND_TYPING_NOTIF_KEY, + SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY, + SETTINGS_12_24_TIMESTAMPS_KEY, + SETTINGS_SHOW_READ_RECEIPTS_KEY, + SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY, + SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY, + SETTINGS_MEDIA_SAVING_PERIOD_KEY, + SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY, + SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY, + SETTINGS_SEND_MESSAGE_WITH_ENTER, + + SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY, + SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY, + // Do not keep SETTINGS_LAZY_LOADING_PREFERENCE_KEY because the user may log in on a server which does not support lazy loading + SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY, + SETTINGS_START_ON_BOOT_PREFERENCE_KEY, + SETTINGS_INTERFACE_TEXT_SIZE_KEY, + SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY, + SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY, + SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY, + + SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY, + SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY, + SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY, + SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY, + SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY, + SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, + SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, + + SETTINGS_USE_RAGE_SHAKE_KEY + ); + + /** + * Clear the preferences. + * + * @param context the context + */ + public static void clearPreferences(Context context) { + Set keysToKeep = new HashSet<>(mKeysToKeepAfterLogout); + + // home server urls + keysToKeep.add(ServerUrlsRepository.HOME_SERVER_URL_PREF); + keysToKeep.add(ServerUrlsRepository.IDENTITY_SERVER_URL_PREF); + + // theme + keysToKeep.add(ThemeUtils.APPLICATION_THEME_KEY); + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = preferences.edit(); + + // get all the existing keys + Set keys = preferences.getAll().keySet(); + // remove the one to keep + + keys.removeAll(keysToKeep); + + for (String key : keys) { + editor.remove(key); + } + + editor.apply(); + } + + /** + * Tells if we have already asked the user to disable battery optimisations on android >= M devices. + * + * @param context the context + * @return true if it was already requested + */ + public static boolean didAskUserToIgnoreBatteryOptimizations(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY, false); + } + + /** + * Mark as requested the question to disable battery optimisations. + * + * @param context the context + */ + public static void setDidAskUserToIgnoreBatteryOptimizations(Context context) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY, true) + .apply(); + } + + public static boolean didMigrateToNotificationRework(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(DID_MIGRATE_TO_NOTIFICATION_REWORK, false); + } + + public static void setDidMigrateToNotificationRework(Context context) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(DID_MIGRATE_TO_NOTIFICATION_REWORK, true) + .apply(); + } + + /** + * Tells if the timestamp must be displayed in 12h format + * + * @param context the context + * @return true if the time must be displayed in 12h format + */ + public static boolean displayTimeIn12hFormat(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_12_24_TIMESTAMPS_KEY, false); + } + + /** + * Tells if the join and leave membership events should be shown in the messages list. + * + * @param context the context + * @return true if the join and leave membership events should be shown in the messages list + */ + public static boolean showJoinLeaveMessages(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY, true); + } + + /** + * Tells if the avatar and display name events should be shown in the messages list. + * + * @param context the context + * @return true true if the avatar and display name events should be shown in the messages list. + */ + public static boolean showAvatarDisplayNameChangeMessages(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY, true); + } + + /** + * Tells the native camera to take a photo or record a video. + * + * @param context the context + * @return true to use the native camera app to record video or take photo. + */ + public static boolean useNativeCamera(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY, false); + } + + /** + * Tells if the send voice feature is enabled. + * + * @param context the context + * @return true if the send voice feature is enabled. + */ + public static boolean isSendVoiceFeatureEnabled(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY, false); + } + + /** + * Tells which compression level to use by default + * + * @param context the context + * @return the selected compression level + */ + public static int getSelectedDefaultMediaCompressionLevel(Context context) { + return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY, "0")); + } + + /** + * Tells which media source to use by default + * + * @param context the context + * @return the selected media source + */ + public static int getSelectedDefaultMediaSource(Context context) { + return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_DEFAULT_MEDIA_SOURCE_KEY, "0")); + } + + /** + * Tells whether to use shutter sound. + * + * @param context the context + * @return true if shutter sound should play + */ + public static boolean useShutterSound(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true); + } + + /** + * Update the notification ringtone + * + * @param context the context + * @param uri the new notification ringtone, or null for no RingTone + */ + public static void setNotificationRingTone(Context context, @Nullable Uri uri) { + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); + + String value = ""; + + if (null != uri) { + value = uri.toString(); + + if (value.startsWith("file://")) { + // it should never happen + // else android.os.FileUriExposedException will be triggered. + // see https://github.com/vector-im/riot-android/issues/1725 + return; + } + } + + editor.putString(SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY, value); + editor.apply(); + } + + /** + * Provides the selected notification ring tone + * + * @param context the context + * @return the selected ring tone or null for no RingTone + */ + @Nullable + public static Uri getNotificationRingTone(Context context) { + String url = PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY, null); + + // the user selects "None" + if (TextUtils.equals(url, "")) { + return null; + } + + Uri uri = null; + + // https://github.com/vector-im/riot-android/issues/1725 + if ((null != url) && !url.startsWith("file://")) { + try { + uri = Uri.parse(url); + } catch (Exception e) { + Timber.e(e, "## getNotificationRingTone() : Uri.parse failed"); + } + } + + if (null == uri) { + uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + } + + Timber.d("## getNotificationRingTone() returns " + uri); + return uri; + } + + /** + * Provide the notification ringtone filename + * + * @param context the context + * @return the filename or null if "None" is selected + */ + @Nullable + public static String getNotificationRingToneName(Context context) { + Uri toneUri = getNotificationRingTone(context); + + if (null == toneUri) { + return null; + } + + String name = null; + + Cursor cursor = null; + + try { + String[] proj = {MediaStore.Audio.Media.DATA}; + cursor = context.getContentResolver().query(toneUri, proj, null, null, null); + int column_index = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA); + cursor.moveToFirst(); + + File file = new File(cursor.getString(column_index)); + name = file.getName(); + + if (name.contains(".")) { + name = name.substring(0, name.lastIndexOf(".")); + } + } catch (Exception e) { + Timber.e(e, "## getNotificationRingToneName() failed() : " + e.getMessage()); + } finally { + if (cursor != null) { + cursor.close(); + } + } + + return name; + } + + /** + * Enable or disable the lazy loading + * + * @param context the context + * @param newValue true to enable lazy loading, false to disable it + */ + public static void setUseLazyLoading(Context context, boolean newValue) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(SETTINGS_LAZY_LOADING_PREFERENCE_KEY, newValue) + .apply(); + } + + /** + * Tells if the lazy loading is enabled + * + * @param context the context + * @return true if the lazy loading of room members is enabled + */ + public static boolean useLazyLoading(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_LAZY_LOADING_PREFERENCE_KEY, false); + } + + /** + * User explicitly refuses the lazy loading. + * + * @param context the context + */ + public static void setUserRefuseLazyLoading(Context context) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY, true) + .apply(); + } + + /** + * Tells if the user has explicitly refused the lazy loading + * + * @param context the context + * @return true if the user has explicitly refuse the lazy loading of room members + */ + public static boolean hasUserRefusedLazyLoading(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY, false); + } + + /** + * Tells if the data save mode is enabled + * + * @param context the context + * @return true if the data save mode is enabled + */ + public static boolean useDataSaveMode(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY, false); + } + + /** + * Tells if the conf calls must be done with Jitsi. + * + * @param context the context + * @return true if the conference call must be done with jitsi. + */ + public static boolean useJitsiConfCall(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY, true); + } + + /** + * Tells if the application is started on boot + * + * @param context the context + * @return true if the application must be started on boot + */ + public static boolean autoStartOnBoot(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_START_ON_BOOT_PREFERENCE_KEY, true); + } + + /** + * Tells if the application is started on boot + * + * @param context the context + * @param value true to start the application on boot + */ + public static void setAutoStartOnBoot(Context context, boolean value) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(SETTINGS_START_ON_BOOT_PREFERENCE_KEY, value) + .apply(); + } + + /** + * Provides the selected saving period. + * + * @param context the context + * @return the selected period + */ + public static int getSelectedMediasSavingPeriod(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getInt(SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY, MEDIA_SAVING_1_WEEK); + } + + /** + * Updates the selected saving period. + * + * @param context the context + * @param index the selected period index + */ + public static void setSelectedMediasSavingPeriod(Context context, int index) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putInt(SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY, index) + .apply(); + } + + /** + * Provides the minimum last access time to keep a media file. + * + * @param context the context + * @return the min last access time (in seconds) + */ + public static long getMinMediasLastAccessTime(Context context) { + int selection = getSelectedMediasSavingPeriod(context); + + switch (selection) { + case MEDIA_SAVING_3_DAYS: + return (System.currentTimeMillis() / 1000) - (3 * 24 * 60 * 60); + case MEDIA_SAVING_1_WEEK: + return (System.currentTimeMillis() / 1000) - (7 * 24 * 60 * 60); + case MEDIA_SAVING_1_MONTH: + return (System.currentTimeMillis() / 1000) - (30 * 24 * 60 * 60); + case MEDIA_SAVING_FOREVER: + return 0; + } + + return 0; + } + + /** + * Provides the selected saving period. + * + * @param context the context + * @return the selected period + */ + public static String getSelectedMediasSavingPeriodString(Context context) { + int selection = getSelectedMediasSavingPeriod(context); + + switch (selection) { + case MEDIA_SAVING_3_DAYS: + return context.getString(R.string.media_saving_period_3_days); + case MEDIA_SAVING_1_WEEK: + return context.getString(R.string.media_saving_period_1_week); + case MEDIA_SAVING_1_MONTH: + return context.getString(R.string.media_saving_period_1_month); + case MEDIA_SAVING_FOREVER: + return context.getString(R.string.media_saving_period_forever); + } + return "?"; + } + + /** + * Fix some migration issues + */ + public static void fixMigrationIssues(Context context) { + // some key names have been updated to supported language switch + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + + if (preferences.contains(context.getString(R.string.settings_pin_missed_notifications))) { + preferences.edit() + .putBoolean(SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY, + preferences.getBoolean(context.getString(R.string.settings_pin_missed_notifications), false)) + .remove(context.getString(R.string.settings_pin_missed_notifications)) + .apply(); + } + + if (preferences.contains(context.getString(R.string.settings_pin_unread_messages))) { + preferences.edit() + .putBoolean(SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY, + preferences.getBoolean(context.getString(R.string.settings_pin_unread_messages), false)) + .remove(context.getString(R.string.settings_pin_unread_messages)) + .apply(); + } + + if (preferences.contains("MARKDOWN_PREFERENCE_KEY")) { + preferences.edit() + .putBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, preferences.getBoolean("MARKDOWN_PREFERENCE_KEY", true)) + .remove("MARKDOWN_PREFERENCE_KEY") + .apply(); + } + + if (preferences.contains("SETTINGS_DONT_SEND_TYPING_NOTIF_KEY")) { + preferences.edit() + .putBoolean(SETTINGS_SEND_TYPING_NOTIF_KEY, !preferences.getBoolean("SETTINGS_DONT_SEND_TYPING_NOTIF_KEY", true)) + .remove("SETTINGS_DONT_SEND_TYPING_NOTIF_KEY") + .apply(); + } + + if (preferences.contains("SETTINGS_DISABLE_MARKDOWN_KEY")) { + preferences.edit() + .putBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, !preferences.getBoolean("SETTINGS_DISABLE_MARKDOWN_KEY", true)) + .remove("SETTINGS_DISABLE_MARKDOWN_KEY") + .apply(); + } + + if (preferences.contains("SETTINGS_HIDE_READ_RECEIPTS")) { + preferences.edit() + .putBoolean(SETTINGS_SHOW_READ_RECEIPTS_KEY, !preferences.getBoolean("SETTINGS_HIDE_READ_RECEIPTS", true)) + .remove("SETTINGS_HIDE_READ_RECEIPTS") + .apply(); + } + + if (preferences.contains("SETTINGS_HIDE_JOIN_LEAVE_MESSAGES_KEY")) { + preferences.edit() + .putBoolean(SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY, !preferences.getBoolean("SETTINGS_HIDE_JOIN_LEAVE_MESSAGES_KEY", true)) + .remove("SETTINGS_HIDE_JOIN_LEAVE_MESSAGES_KEY") + .apply(); + } + + if (preferences.contains("SETTINGS_HIDE_AVATAR_DISPLAY_NAME_CHANGES")) { + preferences.edit() + .putBoolean(SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY, + !preferences.getBoolean("SETTINGS_HIDE_AVATAR_DISPLAY_NAME_CHANGES", true)) + .remove("SETTINGS_HIDE_AVATAR_DISPLAY_NAME_CHANGES") + .apply(); + } + } + + /** + * Tells if the markdown is enabled + * + * @param context the context + * @return true if the markdown is enabled + */ + public static boolean isMarkdownEnabled(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, true); + } + + /** + * Update the markdown enable status. + * + * @param context the context + * @param isEnabled true to enable the markdown + */ + public static void setMarkdownEnabled(Context context, boolean isEnabled) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, isEnabled) + .apply(); + } + + /** + * Tells if the read receipts should be shown + * + * @param context the context + * @return true if the read receipts should be shown + */ + public static boolean showReadReceipts(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SHOW_READ_RECEIPTS_KEY, true); + } + + /** + * Tells if the message timestamps must be always shown + * + * @param context the context + * @return true if the message timestamps must be always shown + */ + public static boolean alwaysShowTimeStamps(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY, false); + } + + /** + * Tells if the typing notifications should be sent + * + * @param context the context + * @return true to send the typing notifs + */ + public static boolean sendTypingNotifs(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SEND_TYPING_NOTIF_KEY, true); + } + + /** + * Tells of the missing notifications rooms must be displayed at left (home screen) + * + * @param context the context + * @return true to move the missed notifications to the left side + */ + public static boolean pinMissedNotifications(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY, true); + } + + /** + * Tells of the unread rooms must be displayed at left (home screen) + * + * @param context the context + * @return true to move the unread room to the left side + */ + public static boolean pinUnreadMessages(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY, true); + } + + /** + * Tells if the phone must vibrate when mentioning + * + * @param context the context + * @return true + */ + public static boolean vibrateWhenMentioning(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_VIBRATE_ON_MENTION_KEY, false); + } + + /** + * Tells if a dialog has been displayed to ask to use the analytics tracking (piwik, matomo, etc.). + * + * @param context the context + * @return true if a dialog has been displayed to ask to use the analytics tracking + */ + public static boolean didAskToUseAnalytics(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY, false); + } + + /** + * To call if the user has been asked for analytics tracking. + * + * @param context the context + */ + public static void setDidAskToUseAnalytics(Context context) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY, true) + .apply(); + } + + /** + * Tells if the analytics tracking is authorized (piwik, matomo, etc.). + * + * @param context the context + * @return true if the analytics tracking is authorized + */ + public static boolean useAnalytics(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_ANALYTICS_KEY, false); + } + + /** + * Enable or disable the analytics tracking. + * + * @param context the context + * @param useAnalytics true to enable the analytics tracking + */ + public static void setUseAnalytics(Context context, boolean useAnalytics) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(SETTINGS_USE_ANALYTICS_KEY, useAnalytics) + .apply(); + } + + /** + * Tells if media should be previewed before sending + * + * @param context the context + * @return true to preview media + */ + public static boolean previewMediaWhenSending(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY, false); + } + + /** + * Tells if message should be send by pressing enter on the soft keyboard + * + * @param context the context + * @return true to send message with enter + */ + public static boolean sendMessageWithEnter(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SEND_MESSAGE_WITH_ENTER, false); + } + + /** + * Tells if the rage shake is used. + * + * @param context the context + * @return true if the rage shake is used + */ + public static boolean useRageshake(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true); + } + + /** + * Update the rage shake status. + * + * @param context the context + * @param isEnabled true to enable the rage shake + */ + public static void setUseRageshake(Context context, boolean isEnabled) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, isEnabled) + .apply(); + } + + /** + * Tells if all the events must be displayed ie even the redacted events. + * + * @param context the context + * @return true to display all the events even the redacted ones. + */ + public static boolean displayAllEvents(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_DISPLAY_ALL_EVENTS_KEY, false); + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt new file mode 100755 index 00000000..f6279d18 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsActivity.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.settings + +import android.content.Context +import android.content.Intent +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import im.vector.matrix.android.api.session.Session +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.RiotActivity +import org.koin.android.ext.android.inject + +/** + * Displays the client settings. + */ +class VectorSettingsActivity : RiotActivity(), + PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, + FragmentManager.OnBackStackChangedListener, + VectorSettingsFragmentInteractionListener { + + private lateinit var vectorSettingsPreferencesFragment: VectorSettingsPreferencesFragment + + override fun getLayoutRes() = R.layout.activity_vector_settings + + override fun getTitleRes() = R.string.title_activity_settings + + private var keyToHighlight: String? = null + + private val session by inject() + + override fun initUiAndData() { + configureToolbar() + + if (isFirstCreation()) { + vectorSettingsPreferencesFragment = VectorSettingsPreferencesFragment.newInstance(session.sessionParams.credentials.userId) + // display the fragment + supportFragmentManager.beginTransaction() + .replace(R.id.vector_settings_page, vectorSettingsPreferencesFragment, FRAGMENT_TAG) + .commit() + } else { + vectorSettingsPreferencesFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as VectorSettingsPreferencesFragment + } + + + supportFragmentManager.addOnBackStackChangedListener(this) + + } + + override fun onDestroy() { + supportFragmentManager.removeOnBackStackChangedListener(this) + super.onDestroy() + } + + override fun onBackStackChanged() { + if (0 == supportFragmentManager.backStackEntryCount) { + supportActionBar?.title = getString(getTitleRes()) + } + } + + override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat?, pref: Preference?): Boolean { + var oFragment: Fragment? = null + + if (PreferencesManager.SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY == pref?.key) { + oFragment = VectorSettingsNotificationsTroubleshootFragment.newInstance(session.sessionParams.credentials.userId) + } else if (PreferencesManager.SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY == pref?.key) { + oFragment = VectorSettingsAdvancedNotificationPreferenceFragment.newInstance(session.sessionParams.credentials.userId) + } + + if (oFragment != null) { + oFragment.setTargetFragment(caller, 0) + // Replace the existing Fragment with the new Fragment + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom, + R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom) + .replace(R.id.vector_settings_page, oFragment, pref?.title.toString()) + .addToBackStack(null) + .commit() + return true + } + return false + } + + + override fun requestHighlightPreferenceKeyOnResume(key: String?) { + keyToHighlight = key + } + + override fun requestedKeyToHighlight(): String? { + return keyToHighlight + } + + companion object { + fun getIntent(context: Context, userId: String) = Intent(context, VectorSettingsActivity::class.java) + .apply { + //putExtra(MXCActionBarActivity.EXTRA_MATRIX_ID, userId) + } + + private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment" + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt new file mode 100644 index 00000000..980859dd --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt @@ -0,0 +1,296 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.settings + +import android.app.Activity +import android.content.Intent +import android.media.RingtoneManager +import android.net.Uri +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import androidx.core.content.edit +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceManager +import im.vector.matrix.android.api.session.Session +import im.vector.riotredesign.R +import im.vector.riotredesign.core.extensions.withArgs +import im.vector.riotredesign.core.platform.RiotActivity +import im.vector.riotredesign.core.preference.BingRule +import im.vector.riotredesign.core.preference.BingRulePreference +import im.vector.riotredesign.features.notifications.NotificationUtils +import im.vector.riotredesign.features.notifications.supportNotificationChannels +import org.koin.android.ext.android.inject + +class VectorSettingsAdvancedNotificationPreferenceFragment : PreferenceFragmentCompat() { + + // members + private val mSession by inject() + private var mLoadingView: View? = null + + // events listener + /* TODO + private val mEventsListener = object : MXEventListener() { + override fun onBingRulesUpdate() { + refreshPreferences() + refreshDisplay() + } + } + */ + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + // define the layout + addPreferencesFromResource(R.xml.vector_settings_notification_advanced_preferences) + + val callNotificationsSystemOptions = findPreference(PreferencesManager.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY) + if (supportNotificationChannels()) { + callNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener { + NotificationUtils.openSystemSettingsForCallCategory(this) + false + } + } else { + callNotificationsSystemOptions.isVisible = false + } + + val noisyNotificationsSystemOptions = findPreference(PreferencesManager.SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY) + if (supportNotificationChannels()) { + noisyNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener { + NotificationUtils.openSystemSettingsForNoisyCategory(this) + false + } + } else { + noisyNotificationsSystemOptions.isVisible = false + } + + val silentNotificationsSystemOptions = findPreference(PreferencesManager.SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY) + if (supportNotificationChannels()) { + silentNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener { + NotificationUtils.openSystemSettingsForSilentCategory(this) + false + } + } else { + silentNotificationsSystemOptions.isVisible = false + } + + + // Ringtone + val ringtonePreference = findPreference(PreferencesManager.SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY) + + if (supportNotificationChannels()) { + ringtonePreference.isVisible = false + } else { + ringtonePreference.summary = PreferencesManager.getNotificationRingToneName(activity) + ringtonePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER) + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION) + + if (null != PreferencesManager.getNotificationRingTone(activity)) { + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, PreferencesManager.getNotificationRingTone(activity)) + } + + startActivityForResult(intent, REQUEST_NOTIFICATION_RINGTONE) + false + } + } + + for (preferenceKey in mPrefKeyToBingRuleId.keys) { + val preference = findPreference(preferenceKey) + if (null != preference) { + if (preference is BingRulePreference) { + //preference.isEnabled = null != rules && isConnected && pushManager.areDeviceNotificationsAllowed() + val rule: BingRule? = null // TODO mSession.dataHandler.pushRules()?.findDefaultRule(mPrefKeyToBingRuleId[preferenceKey]) + + if (rule == null) { + // The rule is not defined, hide the preference + preference.isVisible = false + } else { + preference.isVisible = true + preference.setBingRule(rule) + preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + val rule = preference.createRule(newValue as Int) + if (null != rule) { + /* + TODO + displayLoadingView() + mSession.dataHandler.bingRulesManager.updateRule(preference.rule, + rule, + object : BingRulesManager.onBingRuleUpdateListener { + private fun onDone() { + refreshDisplay() + hideLoadingView() + } + + override fun onBingRuleUpdateSuccess() { + onDone() + } + + override fun onBingRuleUpdateFailure(errorMessage: String) { + activity?.toast(errorMessage) + onDone() + } + }) + */ + } + false + } + } + } + } + } + } + + private fun refreshDisplay() { + listView?.adapter?.notifyDataSetChanged() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == Activity.RESULT_OK) { + when (requestCode) { + REQUEST_NOTIFICATION_RINGTONE -> { + PreferencesManager.setNotificationRingTone(activity, + data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) as Uri?) + + // test if the selected ring tone can be played + val notificationRingToneName = PreferencesManager.getNotificationRingToneName(activity) + if (null != notificationRingToneName) { + PreferencesManager.setNotificationRingTone(activity, PreferencesManager.getNotificationRingTone(activity)) + findPreference(PreferencesManager.SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY).summary = notificationRingToneName + } + } + } + } + } + + override fun onResume() { + super.onResume() + (activity as? RiotActivity)?.supportActionBar?.setTitle(R.string.settings_notification_advanced) + // find the view from parent activity + mLoadingView = activity!!.findViewById(R.id.vector_settings_spinner_views) + + /* TODO + if (mSession.isAlive) { + + mSession.dataHandler.addListener(mEventsListener) + + // refresh anything else + refreshPreferences() + refreshDisplay() + } + */ + } + + override fun onPause() { + super.onPause() + + /* TODO + if (mSession.isAlive) { + mSession.dataHandler.removeListener(mEventsListener) + } + */ + } + + /** + * Refresh the known information about the account + */ + private fun refreshPreferences() { + PreferenceManager.getDefaultSharedPreferences(activity).edit { + /* TODO + mSession.dataHandler.pushRules()?.let { + for (prefKey in mPrefKeyToBingRuleId.keys) { + val preference = findPreference(prefKey) + + if (null != preference && preference is SwitchPreference) { + val ruleId = mPrefKeyToBingRuleId[prefKey] + + val rule = it.findDefaultRule(ruleId) + var isEnabled = null != rule && rule.isEnabled + + if (TextUtils.equals(ruleId, BingRule.RULE_ID_DISABLE_ALL) || TextUtils.equals(ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) { + isEnabled = !isEnabled + } else if (isEnabled) { + val actions = rule!!.actions + + // no action -> noting will be done + if (null == actions || actions.isEmpty()) { + isEnabled = false + } else if (1 == actions.size) { + try { + isEnabled = !TextUtils.equals(actions[0] as String, BingRule.ACTION_DONT_NOTIFY) + } catch (e: Exception) { + Timber.e(LOG_TAG, "## refreshPreferences failed " + e.message, e) + } + + } + }// check if the rule is only defined by don't notify + + putBoolean(prefKey, isEnabled) + } + } + } + */ + } + } + + + //============================================================================================================== + // Display methods + //============================================================================================================== + + /** + * Display the loading view. + */ + private fun displayLoadingView() { + if (null != mLoadingView) { + mLoadingView!!.visibility = View.VISIBLE + } + } + + /** + * Hide the loading view. + */ + private fun hideLoadingView() { + if (null != mLoadingView) { + mLoadingView!!.visibility = View.GONE + } + } + + + /* ========================================================================================== + * Companion + * ========================================================================================== */ + + companion object { + private const val REQUEST_NOTIFICATION_RINGTONE = 888 + + // preference name <-> rule Id + private var mPrefKeyToBingRuleId = mapOf( + PreferencesManager.SETTINGS_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY to BingRule.RULE_ID_CONTAIN_DISPLAY_NAME, + PreferencesManager.SETTINGS_CONTAINING_MY_USER_NAME_PREFERENCE_KEY to BingRule.RULE_ID_CONTAIN_USER_NAME, + PreferencesManager.SETTINGS_MESSAGES_IN_ONE_TO_ONE_PREFERENCE_KEY to BingRule.RULE_ID_ONE_TO_ONE_ROOM, + PreferencesManager.SETTINGS_MESSAGES_IN_GROUP_CHAT_PREFERENCE_KEY to BingRule.RULE_ID_ALL_OTHER_MESSAGES_ROOMS, + PreferencesManager.SETTINGS_INVITED_TO_ROOM_PREFERENCE_KEY to BingRule.RULE_ID_INVITE_ME, + PreferencesManager.SETTINGS_CALL_INVITATIONS_PREFERENCE_KEY to BingRule.RULE_ID_CALL, + PreferencesManager.SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY to BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS + ) + + fun newInstance(matrixId: String) = VectorSettingsAdvancedNotificationPreferenceFragment() + .withArgs { + // putString(MXCActionBarActivity.EXTRA_MATRIX_ID, matrixId) + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsFragmentInteractionListener.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsFragmentInteractionListener.kt new file mode 100644 index 00000000..f141f949 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsFragmentInteractionListener.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.settings + +interface VectorSettingsFragmentInteractionListener { + + fun requestHighlightPreferenceKeyOnResume(key: String?) + + fun requestedKeyToHighlight(): String? + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt new file mode 100644 index 00000000..c7ca4a65 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.settings + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.transition.TransitionManager +import butterknife.BindView +import im.vector.matrix.android.api.session.Session +import im.vector.push.fcm.NotificationTroubleshootTestManagerFactory +import im.vector.riotredesign.R +import im.vector.riotredesign.core.extensions.withArgs +import im.vector.riotredesign.core.platform.RiotActivity +import im.vector.riotredesign.core.platform.RiotFragment +import im.vector.riotredesign.features.rageshake.BugReporter +import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager +import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest +import org.koin.android.ext.android.inject + +class VectorSettingsNotificationsTroubleshootFragment : RiotFragment() { + + @BindView(R.id.troubleshoot_test_recycler_view) + lateinit var mRecyclerView: RecyclerView + @BindView(R.id.troubleshoot_bottom_view) + lateinit var mBottomView: ViewGroup + @BindView(R.id.toubleshoot_summ_description) + lateinit var mSummaryDescription: TextView + @BindView(R.id.troubleshoot_summ_button) + lateinit var mSummaryButton: Button + @BindView(R.id.troubleshoot_run_button) + lateinit var mRunButton: Button + + private var testManager: NotificationTroubleshootTestManager? = null + // members + private val mSession by inject() + + override fun getLayoutResId() = R.layout.fragment_settings_notifications_troubleshoot + + private var interactionListener: VectorSettingsFragmentInteractionListener? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val appContext = activity!!.applicationContext + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val layoutManager = LinearLayoutManager(context) + mRecyclerView.layoutManager = layoutManager + + val dividerItemDecoration = DividerItemDecoration(mRecyclerView.context, + layoutManager.orientation) + mRecyclerView.addItemDecoration(dividerItemDecoration) + + + mSummaryButton.setOnClickListener { + BugReporter.openBugReportScreen(activity!!) + } + + mRunButton.setOnClickListener { + testManager?.retry() + } + startUI() + } + + private fun startUI() { + + mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_running_status, + 0, 0) + + testManager = NotificationTroubleshootTestManagerFactory.createTestManager(this, mSession) + + testManager?.statusListener = { troubleshootTestManager -> + if (isAdded) { + TransitionManager.beginDelayedTransition(mBottomView) + when (troubleshootTestManager.diagStatus) { + TroubleshootTest.TestStatus.NOT_STARTED -> { + mSummaryDescription.text = "" + mSummaryButton.visibility = View.GONE + mRunButton.visibility = View.VISIBLE + } + TroubleshootTest.TestStatus.RUNNING -> { + //Forces int type because it's breaking lint + val size: Int = troubleshootTestManager.testList.size + val currentTestIndex: Int = troubleshootTestManager.currentTestIndex + mSummaryDescription.text = getString( + R.string.settings_troubleshoot_diagnostic_running_status, + currentTestIndex, + size + ) + mSummaryButton.visibility = View.GONE + mRunButton.visibility = View.GONE + } + TroubleshootTest.TestStatus.FAILED -> { + //check if there are quick fixes + var hasQuickFix = false + testManager?.testList?.let { + for (test in it) { + if (test.status == TroubleshootTest.TestStatus.FAILED && test.quickFix != null) { + hasQuickFix = true + break + } + } + } + if (hasQuickFix) { + mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_failure_status_with_quickfix) + } else { + mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_failure_status_no_quickfix) + } + mSummaryButton.visibility = View.VISIBLE + mRunButton.visibility = View.VISIBLE + } + TroubleshootTest.TestStatus.SUCCESS -> { + mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_success_status) + mSummaryButton.visibility = View.VISIBLE + mRunButton.visibility = View.VISIBLE + } + } + } + + } + mRecyclerView.adapter = testManager?.adapter + testManager?.runDiagnostic() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (resultCode == Activity.RESULT_OK && requestCode == NotificationTroubleshootTestManager.REQ_CODE_FIX) { + testManager?.retry() + return + } + super.onActivityResult(requestCode, resultCode, data) + } + + override fun onDetach() { + testManager?.cancel() + interactionListener = null + super.onDetach() + } + + override fun onResume() { + super.onResume() + (activity as? RiotActivity)?.supportActionBar?.setTitle(R.string.settings_notification_troubleshoot) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is VectorSettingsFragmentInteractionListener) { + interactionListener = context + } + } + + companion object { + // static constructor + fun newInstance(matrixId: String) = VectorSettingsNotificationsTroubleshootFragment() + .withArgs { + // TODO putString(MXCActionBarActivity.EXTRA_MATRIX_ID, matrixId) + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt new file mode 100755 index 00000000..24df2bf5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt @@ -0,0 +1,2927 @@ +/* + * Copyright 2018 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.settings + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.SharedPreferences +import android.media.RingtoneManager +import android.net.Uri +import android.os.AsyncTask +import android.os.Bundle +import android.provider.Settings +import android.text.Editable +import android.text.TextUtils +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.* +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.core.content.edit +import androidx.core.view.isVisible +import androidx.preference.* +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import im.vector.matrix.android.api.session.Session +import im.vector.riotredesign.R +import im.vector.riotredesign.core.extensions.showPassword +import im.vector.riotredesign.core.extensions.withArgs +import im.vector.riotredesign.core.platform.SimpleTextWatcher +import im.vector.riotredesign.core.preference.BingRule +import im.vector.riotredesign.core.preference.ProgressBarPreference +import im.vector.riotredesign.core.preference.UserAvatarPreference +import im.vector.riotredesign.core.preference.VectorPreference +import im.vector.riotredesign.core.utils.* +import im.vector.riotredesign.features.themes.ThemeUtils +import org.koin.android.ext.android.inject +import java.lang.ref.WeakReference +import java.util.* + +class VectorSettingsPreferencesFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { + + // members + private val mSession by inject() + + // disable some updates if there is + // TODO private val mNetworkListener = IMXNetworkEventListener { refreshDisplay() } + // events listener + // TODO private val mEventsListener = object : MXEventListener() { + // TODO override fun onBingRulesUpdate() { + // TODO refreshPreferences() + // TODO refreshDisplay() + // TODO } + + // TODO override fun onAccountInfoUpdate(myUser: MyUser) { + // TODO // refresh the settings value + // TODO PreferenceManager.getDefaultSharedPreferences(VectorApp.getInstance().applicationContext).edit { + // TODO putString(PreferencesManager.SETTINGS_DISPLAY_NAME_PREFERENCE_KEY, myUser.displayname) + // TODO } + + // TODO refreshDisplay() + // TODO } + // TODO } + + private var mLoadingView: View? = null + + private var mDisplayedEmails = ArrayList() + private var mDisplayedPhoneNumber = ArrayList() + + // TODO private var mMyDeviceInfo: DeviceInfo? = null + + // TODO private var mDisplayedPushers = ArrayList() + + private var interactionListener: VectorSettingsFragmentInteractionListener? = null + + // devices: device IDs and device names + // TODO private var mDevicesNameList: List = ArrayList() + // used to avoid requesting to enter the password for each deletion + private var mAccountPassword: String? = null + + // current publicised group list + private var mPublicisedGroups: MutableSet? = null + + /* ========================================================================================== + * Preferences + * ========================================================================================== */ + + private val mUserSettingsCategory by lazy { + findPreference(PreferencesManager.SETTINGS_USER_SETTINGS_PREFERENCE_KEY) as PreferenceCategory + } + private val mUserAvatarPreference by lazy { + findPreference(PreferencesManager.SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY) as UserAvatarPreference + } + private val mDisplayNamePreference by lazy { + findPreference(PreferencesManager.SETTINGS_DISPLAY_NAME_PREFERENCE_KEY) as EditTextPreference + } + private val mPasswordPreference by lazy { + findPreference(PreferencesManager.SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY) + } + + // Local contacts + private val mContactSettingsCategory by lazy { + findPreference(PreferencesManager.SETTINGS_CONTACT_PREFERENCE_KEYS) as PreferenceCategory + } + + private val mContactPhonebookCountryPreference by lazy { + findPreference(PreferencesManager.SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY) + } + + // Group Flairs + private val mGroupsFlairCategory by lazy { + findPreference(PreferencesManager.SETTINGS_GROUPS_FLAIR_KEY) as PreferenceCategory + } + + // cryptography + private val mCryptographyCategory by lazy { + findPreference(PreferencesManager.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY) as PreferenceCategory + } + private val mCryptographyCategoryDivider by lazy { + findPreference(PreferencesManager.SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY) + } + // cryptography manage + private val mCryptographyManageCategory by lazy { + findPreference(PreferencesManager.SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY) as PreferenceCategory + } + private val mCryptographyManageCategoryDivider by lazy { + findPreference(PreferencesManager.SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY) + } + // displayed pushers + private val mPushersSettingsDivider by lazy { + findPreference(PreferencesManager.SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY) + } + private val mPushersSettingsCategory by lazy { + findPreference(PreferencesManager.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY) as PreferenceCategory + } + private val mDevicesListSettingsCategory by lazy { + findPreference(PreferencesManager.SETTINGS_DEVICES_LIST_PREFERENCE_KEY) as PreferenceCategory + } + private val mDevicesListSettingsCategoryDivider by lazy { + findPreference(PreferencesManager.SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY) + } + // displayed the ignored users list + private val mIgnoredUserSettingsCategoryDivider by lazy { + findPreference(PreferencesManager.SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY) + } + private val mIgnoredUserSettingsCategory by lazy { + findPreference(PreferencesManager.SETTINGS_IGNORED_USERS_PREFERENCE_KEY) as PreferenceCategory + } + // background sync category + private val mSyncRequestTimeoutPreference by lazy { + // ? Cause it can be removed + findPreference(PreferencesManager.SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY) as EditTextPreference? + } + private val mSyncRequestDelayPreference by lazy { + // ? Cause it can be removed + findPreference(PreferencesManager.SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY) as EditTextPreference? + } + private val mLabsCategory by lazy { + findPreference(PreferencesManager.SETTINGS_LABS_PREFERENCE_KEY) as PreferenceCategory + } + private val backgroundSyncCategory by lazy { + findPreference(PreferencesManager.SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY) + } + private val backgroundSyncDivider by lazy { + findPreference(PreferencesManager.SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY) + } + private val backgroundSyncPreference by lazy { + findPreference(PreferencesManager.SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY) as SwitchPreference + } + private val mUseRiotCallRingtonePreference by lazy { + findPreference(PreferencesManager.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY) as SwitchPreference + } + private val mCallRingtonePreference by lazy { + findPreference(PreferencesManager.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY) + } + private val notificationsSettingsCategory by lazy { + findPreference(PreferencesManager.SETTINGS_NOTIFICATIONS_KEY) as PreferenceCategory + } + private val mNotificationPrivacyPreference by lazy { + findPreference(PreferencesManager.SETTINGS_NOTIFICATION_PRIVACY_PREFERENCE_KEY) + } + private val selectedLanguagePreference by lazy { + findPreference(PreferencesManager.SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY) + } + private val textSizePreference by lazy { + findPreference(PreferencesManager.SETTINGS_INTERFACE_TEXT_SIZE_KEY) + } + private val cryptoInfoDeviceNamePreference by lazy { + findPreference(PreferencesManager.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY) as VectorPreference + } + private val cryptoInfoDeviceIdPreference by lazy { + findPreference(PreferencesManager.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY) + } + + private val manageBackupPref by lazy { + findPreference(PreferencesManager.SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY) + } + + private val exportPref by lazy { + findPreference(PreferencesManager.SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY) + } + + private val importPref by lazy { + findPreference(PreferencesManager.SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY) + } + + private val cryptoInfoTextPreference by lazy { + findPreference(PreferencesManager.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY) + } + // encrypt to unverified devices + private val sendToUnverifiedDevicesPref by lazy { + findPreference(PreferencesManager.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY) as SwitchPreference + } + + /* ========================================================================================== + * Life cycle + * ========================================================================================== */ + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + val appContext = activity?.applicationContext + + // retrieve the arguments + /* + val sessionArg = Matrix.getInstance(appContext).getSession(arguments?.getString(ARG_MATRIX_ID)) + + // sanity checks + if (null == sessionArg || !sessionArg.isAlive) { + activity?.finish() + return + } + + mSession = sessionArg + */ + + // define the layout + addPreferencesFromResource(R.xml.vector_settings_preferences) + + // Avatar + mUserAvatarPreference.let { + it.setSession(mSession) + it.onPreferenceClickListener = Preference.OnPreferenceClickListener { + onUpdateAvatarClick() + false + } + } + + // Display name + mDisplayNamePreference.let { + it.summary = "TODO" // mSession.myUser.displayname + it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + onDisplayNameClick(newValue?.let { (it as String).trim() }) + false + } + } + + // Password + mPasswordPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + onPasswordUpdateClick() + false + } + + // Add Email + (findPreference(ADD_EMAIL_PREFERENCE_KEY) as EditTextPreference).let { + // It does not work on XML, do it here + it.icon = activity?.let { + ThemeUtils.tintDrawable(it, + ContextCompat.getDrawable(it, R.drawable.ic_add_black)!!, R.attr.vctr_settings_icon_tint_color) + } + + // Unfortunately, this is not supported in lib v7 + // it.editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + + it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + addEmail((newValue as String).trim()) + false + } + } + + // Add phone number + findPreference(ADD_PHONE_NUMBER_PREFERENCE_KEY).let { + // It does not work on XML, do it here + it.icon = activity?.let { + ThemeUtils.tintDrawable(it, + ContextCompat.getDrawable(it, R.drawable.ic_add_black)!!, R.attr.vctr_settings_icon_tint_color) + } + + it.setOnPreferenceClickListener { + // TODO val intent = PhoneNumberAdditionActivity.getIntent(activity, mSession.credentials.userId) + // startActivityForResult(intent, REQUEST_NEW_PHONE_NUMBER) + true + } + } + + refreshEmailsList() + refreshPhoneNumbersList() + + // Contacts + setContactsPreferences() + + // user interface preferences + setUserInterfacePreferences() + + // Url preview + (findPreference(PreferencesManager.SETTINGS_SHOW_URL_PREVIEW_KEY) as SwitchPreference).let { + /* + TODO + it.isChecked = mSession.isURLPreviewEnabled + + it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + if (null != newValue && newValue as Boolean != mSession.isURLPreviewEnabled) { + displayLoadingView() + mSession.setURLPreviewStatus(newValue, object : ApiCallback { + override fun onSuccess(info: Void?) { + it.isChecked = mSession.isURLPreviewEnabled + hideLoadingView() + } + + private fun onError(errorMessage: String) { + activity?.toast(errorMessage) + + onSuccess(null) + } + + override fun onNetworkError(e: Exception) { + onError(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + onError(e.localizedMessage) + } + + override fun onUnexpectedError(e: Exception) { + onError(e.localizedMessage) + } + }) + } + + false + } + */ + } + + // Themes + findPreference(ThemeUtils.APPLICATION_THEME_KEY) + .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + if (newValue is String) { + // TODO VectorApp.updateApplicationTheme(newValue) + activity?.let { + it.startActivity(it.intent) + it.finish() + } + true + } else { + false + } + } + + // Flair + refreshGroupFlairsList() + + // push rules + + // Notification privacy + mNotificationPrivacyPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + // TODO startActivity(NotificationPrivacyActivity.getIntent(activity)) + true + } + refreshNotificationPrivacy() + + for (preferenceKey in mPrefKeyToBingRuleId.keys) { + val preference = findPreference(preferenceKey) + + if (null != preference) { + if (preference is SwitchPreference) { + preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValueAsVoid -> + // on some old android APIs, + // the callback is called even if there is no user interaction + // so the value will be checked to ensure there is really no update. + onPushRuleClick(preference.key, newValueAsVoid as Boolean) + true + } + } + } + } + + // background sync tuning settings + // these settings are useless and hidden if the app is registered to the FCM push service + /* + TODO + val pushManager = Matrix.getInstance(appContext).pushManager + if (pushManager.useFcm() && pushManager.hasRegistrationToken()) { + // Hide the section + preferenceScreen.removePreference(backgroundSyncDivider) + preferenceScreen.removePreference(backgroundSyncCategory) + } else { + backgroundSyncPreference.let { + it.isChecked = pushManager.isBackgroundSyncAllowed + + it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, aNewValue -> + val newValue = aNewValue as Boolean + + if (newValue != pushManager.isBackgroundSyncAllowed) { + pushManager.isBackgroundSyncAllowed = newValue + } + + displayLoadingView() + + Matrix.getInstance(activity)?.pushManager?.forceSessionsRegistration(object : ApiCallback { + override fun onSuccess(info: Void?) { + hideLoadingView() + } + + override fun onMatrixError(e: MatrixError?) { + hideLoadingView() + } + + override fun onNetworkError(e: java.lang.Exception?) { + hideLoadingView() + } + + override fun onUnexpectedError(e: java.lang.Exception?) { + hideLoadingView() + } + }) + + true + } + } + } + */ + + // Push target + refreshPushersList() + + // Ignore users + refreshIgnoredUsersList() + + // Lab + val useCryptoPref = findPreference(PreferencesManager.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY) as SwitchPreference + val cryptoIsEnabledPref = findPreference(PreferencesManager.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY) + + + if (mSession.isCryptoEnabled()) { + mLabsCategory.removePreference(useCryptoPref) + + cryptoIsEnabledPref.isEnabled = false + } else { + mLabsCategory.removePreference(cryptoIsEnabledPref) + + useCryptoPref.isChecked = false + + useCryptoPref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValueAsVoid -> + if (TextUtils.isEmpty(mSession.sessionParams.credentials.deviceId)) { + activity?.let { activity -> + AlertDialog.Builder(activity) + .setMessage(R.string.room_settings_labs_end_to_end_warnings) + .setPositiveButton(R.string.logout) { _, _ -> + // TODO CommonActivityUtils.logout(activity) + } + .setNegativeButton(R.string.cancel) { _, _ -> + useCryptoPref.isChecked = false + } + .setOnCancelListener { + useCryptoPref.isChecked = false + } + .show() + } + } else { + val newValue = newValueAsVoid as Boolean + + if (mSession.isCryptoEnabled() != newValue) { + /* TODO + displayLoadingView() + + mSession.enableCrypto(newValue, object : ApiCallback { + private fun refresh() { + activity?.runOnUiThread { + hideLoadingView() + useCryptoPref.isChecked = mSession.isCryptoEnabled + + if (mSession.isCryptoEnabled) { + mLabsCategory.removePreference(useCryptoPref) + mLabsCategory.addPreference(cryptoIsEnabledPref) + } + } + } + + override fun onSuccess(info: Void?) { + useCryptoPref.isEnabled = false + refresh() + } + + override fun onNetworkError(e: Exception) { + useCryptoPref.isChecked = false + } + + override fun onMatrixError(e: MatrixError) { + useCryptoPref.isChecked = false + } + + override fun onUnexpectedError(e: Exception) { + useCryptoPref.isChecked = false + } + }) + */ + } + } + + true + } + } + + // SaveMode Management + findPreference(PreferencesManager.SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY) + .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + /* TODO + val sessions = Matrix.getMXSessions(activity) + for (session in sessions) { + session.setUseDataSaveMode(newValue as Boolean) + } + */ + + true + } + + // Device list + refreshDevicesList() + + // Advanced settings + + // user account + findPreference(PreferencesManager.SETTINGS_LOGGED_IN_PREFERENCE_KEY) + .summary = mSession.sessionParams.credentials.userId + + // home server + findPreference(PreferencesManager.SETTINGS_HOME_SERVER_PREFERENCE_KEY) + .summary = mSession.sessionParams.homeServerConnectionConfig.homeServerUri.toString() + + // identity server + findPreference(PreferencesManager.SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY) + .summary = mSession.sessionParams.homeServerConnectionConfig.identityServerUri.toString() + + // Analytics + + // Analytics tracking management + (findPreference(PreferencesManager.SETTINGS_USE_ANALYTICS_KEY) as SwitchPreference).let { + // On if the analytics tracking is activated + it.isChecked = PreferencesManager.useAnalytics(appContext) + + it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + PreferencesManager.setUseAnalytics(appContext, newValue as Boolean) + true + } + } + + // Rageshake Management + (findPreference(PreferencesManager.SETTINGS_USE_RAGE_SHAKE_KEY) as SwitchPreference).let { + it.isChecked = PreferencesManager.useRageshake(appContext) + + it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + PreferencesManager.setUseRageshake(appContext, newValue as Boolean) + true + } + } + + // preference to start the App info screen, to facilitate App permissions access + findPreference(APP_INFO_LINK_PREFERENCE_KEY) + .onPreferenceClickListener = Preference.OnPreferenceClickListener { + + activity?.let { + val intent = Intent().apply { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + val uri = appContext?.let { Uri.fromParts("package", it.packageName, null) } + + data = uri + } + it.applicationContext.startActivity(intent) + } + + true + } + + // application version + (findPreference(PreferencesManager.SETTINGS_VERSION_PREFERENCE_KEY)).let { + it.summary = "TODO" // VectorUtils.getApplicationVersion(appContext) + + it.setOnPreferenceClickListener { + appContext?.let { + copyToClipboard(it, "TODO") //VectorUtils.getApplicationVersion(it)) + } + true + } + } + + // olm version + findPreference(PreferencesManager.SETTINGS_OLM_VERSION_PREFERENCE_KEY) + // TODO .summary = mSession.getCryptoVersion(appContext, false) + + // copyright + findPreference(PreferencesManager.SETTINGS_COPYRIGHT_PREFERENCE_KEY) + .onPreferenceClickListener = Preference.OnPreferenceClickListener { + // TODO VectorUtils.displayAppCopyright() + false + } + + // terms & conditions + findPreference(PreferencesManager.SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY) + .onPreferenceClickListener = Preference.OnPreferenceClickListener { + // TODO VectorUtils.displayAppTac() + false + } + + // privacy policy + findPreference(PreferencesManager.SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY) + .onPreferenceClickListener = Preference.OnPreferenceClickListener { + // TODO VectorUtils.displayAppPrivacyPolicy() + false + } + + // third party notice + findPreference(PreferencesManager.SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY) + .onPreferenceClickListener = Preference.OnPreferenceClickListener { + // TODO VectorUtils.displayThirdPartyLicenses() + false + } + + // update keep medias period + findPreference(PreferencesManager.SETTINGS_MEDIA_SAVING_PERIOD_KEY).let { + it.summary = PreferencesManager.getSelectedMediasSavingPeriodString(activity) + + it.onPreferenceClickListener = Preference.OnPreferenceClickListener { + context?.let { context: Context -> + AlertDialog.Builder(context) + .setSingleChoiceItems(R.array.media_saving_choice, + PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n -> + PreferencesManager.setSelectedMediasSavingPeriod(activity, n) + d.cancel() + + it.summary = PreferencesManager.getSelectedMediasSavingPeriodString(activity) + } + .show() + } + + false + } + } + + // clear medias cache + findPreference(PreferencesManager.SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY).let { + /* + TODO + MXMediaCache.getCachesSize(activity, object : SimpleApiCallback() { + override fun onSuccess(size: Long) { + if (null != activity) { + it.summary = android.text.format.Formatter.formatFileSize(activity, size) + } + } + }) + + it.onPreferenceClickListener = Preference.OnPreferenceClickListener { + displayLoadingView() + + val task = ClearMediaCacheAsyncTask( + backgroundTask = { + mSession.mediaCache.clear() + activity?.let { it -> Glide.get(it).clearDiskCache() } + }, + onCompleteTask = { + hideLoadingView() + + MXMediaCache.getCachesSize(activity, object : SimpleApiCallback() { + override fun onSuccess(size: Long) { + it.summary = Formatter.formatFileSize(activity, size) + } + }) + } + ) + + try { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) + } catch (e: Exception) { + Timber.e(e, "## mSession.getMediaCache().clear() failed " + e.message) + task.cancel(true) + hideLoadingView() + } + + false + } + */ + } + + // Incoming call sounds + mUseRiotCallRingtonePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + activity?.let { setUseRiotDefaultRingtone(it, mUseRiotCallRingtonePreference.isChecked) } + false + } + + mCallRingtonePreference.let { + activity?.let { activity -> it.summary = getCallRingtoneName(activity) } + it.onPreferenceClickListener = Preference.OnPreferenceClickListener { + displayRingtonePicker() + false + } + } + + // clear cache + findPreference(PreferencesManager.SETTINGS_CLEAR_CACHE_PREFERENCE_KEY).let { + /* + TODO + MXSession.getApplicationSizeCaches(activity, object : SimpleApiCallback() { + override fun onSuccess(size: Long) { + if (null != activity) { + it.summary = android.text.format.Formatter.formatFileSize(activity, size) + } + } + }) + */ + + it.onPreferenceClickListener = Preference.OnPreferenceClickListener { + displayLoadingView() + // TODO Matrix.getInstance(appContext).reloadSessions(appContext) + false + } + } + + // Deactivate account section + + // deactivate account + findPreference(PreferencesManager.SETTINGS_DEACTIVATE_ACCOUNT_KEY) + .onPreferenceClickListener = Preference.OnPreferenceClickListener { + activity?.let { + // TODO startActivity(DeactivateAccountActivity.getIntent(it)) + } + + false + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = super.onCreateView(inflater, container, savedInstanceState) + + view?.apply { + val listView = findViewById(android.R.id.list) + listView?.setPadding(0, 0, 0, 0) + } + + return view + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + // if the user toggles the contacts book permission + /* TODO + if (TextUtils.equals(key, ContactsManager.CONTACTS_BOOK_ACCESS_KEY)) { + // reset the current snapshot + ContactsManager.getInstance().clearSnapshot() + } + */ + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is VectorSettingsFragmentInteractionListener) { + interactionListener = context + } + } + + override fun onDetach() { + interactionListener = null + super.onDetach() + } + + override fun onResume() { + super.onResume() + + // find the view from parent activity + // TODO mLoadingView = activity?.findViewById(R.id.vector_settings_spinner_views) + + /* TODO + if (mSession.isAlive) { + val context = activity?.applicationContext + + mSession.dataHandler.addListener(mEventsListener) + + Matrix.getInstance(context)?.addNetworkEventListener(mNetworkListener) + + mSession.myUser.refreshThirdPartyIdentifiers(object : SimpleApiCallback() { + override fun onSuccess(info: Void?) { + // ensure that the activity still exists + // and the result is called in the right thread + activity?.runOnUiThread { + refreshEmailsList() + refreshPhoneNumbersList() + } + } + }) + + Matrix.getInstance(context)?.pushManager?.refreshPushersList(Matrix.getInstance(context)?.sessions, object : SimpleApiCallback(activity) { + override fun onSuccess(info: Void?) { + refreshPushersList() + } + }) + + PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this) + + // refresh anything else + refreshPreferences() + refreshNotificationPrivacy() + refreshDisplay() + refreshBackgroundSyncPrefs() + } + */ + + interactionListener?.requestedKeyToHighlight()?.let { key -> + interactionListener?.requestHighlightPreferenceKeyOnResume(null) + val preference = findPreference(key) + (preference as? VectorPreference)?.isHighlighted = true + } + } + + override fun onPause() { + super.onPause() + + val context = activity?.applicationContext + + /* TODO + if (mSession.isAlive) { + mSession.dataHandler.removeListener(mEventsListener) + Matrix.getInstance(context)?.removeNetworkEventListener(mNetworkListener) + } + */ + + PreferenceManager.getDefaultSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(this) + } + + // TODO Test + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + /* TODO + if (allGranted(grantResults)) { + if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) { + changeAvatar() + } else if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) { + exportKeys() + } + } + */ + } + + //============================================================================================================== + // Display methods + //============================================================================================================== + + /** + * Display the loading view. + */ + private fun displayLoadingView() { + // search the loading view from the upper view + if (null == mLoadingView) { + var parent = view + + while (parent != null && mLoadingView == null) { + // TODO mLoadingView = parent.findViewById(R.id.vector_settings_spinner_views) + parent = parent.parent as View + } + } else { + mLoadingView?.visibility = View.VISIBLE + } + } + + /** + * Hide the loading view. + */ + private fun hideLoadingView() { + mLoadingView?.visibility = View.GONE + } + + /** + * Hide the loading view and refresh the preferences. + * + * @param refresh true to refresh the display + */ + private fun hideLoadingView(refresh: Boolean) { + mLoadingView?.visibility = View.GONE + + if (refresh) { + refreshDisplay() + } + } + + /** + * Refresh the preferences. + */ + private fun refreshDisplay() { + /* TODO + // If Matrix instance is null, then connection can't be there + val isConnected = Matrix.getInstance(activity)?.isConnected ?: false + val appContext = activity?.applicationContext + + val preferenceManager = preferenceManager + + // refresh the avatar + mUserAvatarPreference.refreshAvatar() + mUserAvatarPreference.isEnabled = isConnected + + // refresh the display name + mDisplayNamePreference.summary = mSession.myUser.displayname + mDisplayNamePreference.text = mSession.myUser.displayname + mDisplayNamePreference.isEnabled = isConnected + + // change password + mPasswordPreference.isEnabled = isConnected + + // update the push rules + val preferences = PreferenceManager.getDefaultSharedPreferences(appContext) + + val rules = mSession.dataHandler.pushRules() + + val pushManager = Matrix.getInstance(appContext)?.pushManager + + for (preferenceKey in mPrefKeyToBingRuleId.keys) { + val preference = preferenceManager.findPreference(preferenceKey) + + if (null != preference) { + + if (preference is SwitchPreference) { + when (preferenceKey) { + PreferencesManager.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY -> + preference.isChecked = pushManager?.areDeviceNotificationsAllowed() ?: true + + PreferencesManager.SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY -> { + preference.isChecked = pushManager?.isScreenTurnedOn ?: false + preference.isEnabled = pushManager?.areDeviceNotificationsAllowed() ?: true + } + else -> { + preference.isEnabled = null != rules && isConnected + preference.isChecked = preferences.getBoolean(preferenceKey, false) + } + } + } + } + } + + // If notifications are disabled for the current user account or for the current user device + // The others notifications settings have to be disable too + val areNotificationAllowed = rules?.findDefaultRule(BingRule.RULE_ID_DISABLE_ALL)?.isEnabled == true + + mNotificationPrivacyPreference.isEnabled = !areNotificationAllowed + && (pushManager?.areDeviceNotificationsAllowed() ?: true) && pushManager?.useFcm() ?: true + */ + } + + //============================================================================================================== + // Update items methods + //============================================================================================================== + + /** + * Update the password. + */ + private fun onPasswordUpdateClick() { + activity?.let { activity -> + val view: ViewGroup = activity.layoutInflater.inflate(R.layout.dialog_change_password, null) as ViewGroup + + val showPassword: ImageView = view.findViewById(R.id.change_password_show_passwords) + val oldPasswordTil: TextInputLayout = view.findViewById(R.id.change_password_old_pwd_til) + val oldPasswordText: TextInputEditText = view.findViewById(R.id.change_password_old_pwd_text) + val newPasswordText: TextInputEditText = view.findViewById(R.id.change_password_new_pwd_text) + val confirmNewPasswordTil: TextInputLayout = view.findViewById(R.id.change_password_confirm_new_pwd_til) + val confirmNewPasswordText: TextInputEditText = view.findViewById(R.id.change_password_confirm_new_pwd_text) + val changePasswordLoader: View = view.findViewById(R.id.change_password_loader) + + var passwordShown = false + + showPassword.setOnClickListener(object : View.OnClickListener { + override fun onClick(v: View?) { + passwordShown = !passwordShown + + oldPasswordText.showPassword(passwordShown) + newPasswordText.showPassword(passwordShown) + confirmNewPasswordText.showPassword(passwordShown) + + showPassword.setImageResource(if (passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + } + }) + + val dialog = AlertDialog.Builder(activity) + .setView(view) + .setPositiveButton(R.string.settings_change_password_submit, null) + .setNegativeButton(R.string.cancel, null) + .setOnDismissListener { + val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(view.applicationWindowToken, 0) + } + .create() + + dialog.setOnShowListener { + val updateButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) + updateButton.isEnabled = false + + fun updateUi() { + val oldPwd = oldPasswordText.text.toString().trim() + val newPwd = newPasswordText.text.toString().trim() + val newConfirmPwd = confirmNewPasswordText.text.toString().trim() + + updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty() && TextUtils.equals(newPwd, newConfirmPwd) + + if (newPwd.isNotEmpty() && newConfirmPwd.isNotEmpty() && !TextUtils.equals(newPwd, newConfirmPwd)) { + confirmNewPasswordTil.error = getString(R.string.passwords_do_not_match) + } + } + + oldPasswordText.addTextChangedListener(object : SimpleTextWatcher() { + override fun afterTextChanged(s: Editable) { + oldPasswordTil.error = null + updateUi() + } + }) + + newPasswordText.addTextChangedListener(object : SimpleTextWatcher() { + override fun afterTextChanged(s: Editable) { + confirmNewPasswordTil.error = null + updateUi() + } + }) + + confirmNewPasswordText.addTextChangedListener(object : SimpleTextWatcher() { + override fun afterTextChanged(s: Editable) { + confirmNewPasswordTil.error = null + updateUi() + } + }) + + fun showPasswordLoadingView(toShow: Boolean) { + if (toShow) { + showPassword.isEnabled = false + oldPasswordText.isEnabled = false + newPasswordText.isEnabled = false + confirmNewPasswordText.isEnabled = false + changePasswordLoader.isVisible = true + updateButton.isEnabled = false + } else { + showPassword.isEnabled = true + oldPasswordText.isEnabled = true + newPasswordText.isEnabled = true + confirmNewPasswordText.isEnabled = true + changePasswordLoader.isVisible = false + updateButton.isEnabled = true + } + } + + updateButton.setOnClickListener { + if (passwordShown) { + // Hide passwords during processing + showPassword.performClick() + } + + val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(view.applicationWindowToken, 0) + + val oldPwd = oldPasswordText.text.toString().trim() + val newPwd = newPasswordText.text.toString().trim() + + /* TODO + showPasswordLoadingView(true) + + mSession.updatePassword(oldPwd, newPwd, object : ApiCallback { + private fun onDone(@StringRes textResId: Int) { + showPasswordLoadingView(false) + + if (textResId == R.string.settings_fail_to_update_password_invalid_current_password) { + oldPasswordTil.error = getString(textResId) + } else { + dialog.dismiss() + activity.toast(textResId, Toast.LENGTH_LONG) + } + } + + override fun onSuccess(info: Void?) { + onDone(R.string.settings_password_updated) + } + + override fun onNetworkError(e: Exception) { + onDone(R.string.settings_fail_to_update_password) + } + + override fun onMatrixError(e: MatrixError) { + if (e.error == "Invalid password") { + onDone(R.string.settings_fail_to_update_password_invalid_current_password) + } else { + dialog.dismiss() + onDone(R.string.settings_fail_to_update_password) + } + } + + override fun onUnexpectedError(e: Exception) { + onDone(R.string.settings_fail_to_update_password) + } + }) + */ + } + } + dialog.show() + } + } + + /** + * Update a push rule. + */ + + private fun onPushRuleClick(preferenceKey: String, newValue: Boolean) { + /* TODO + val matrixInstance = Matrix.getInstance(context) + val pushManager = matrixInstance.pushManager + + Timber.d("onPushRuleClick $preferenceKey : set to $newValue") + + when (preferenceKey) { + + PreferencesManager.SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY -> { + if (pushManager.isScreenTurnedOn != newValue) { + pushManager.isScreenTurnedOn = newValue + } + } + + PreferencesManager.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY -> { + val isConnected = matrixInstance.isConnected + val isAllowed = pushManager.areDeviceNotificationsAllowed() + + // avoid useless update + if (isAllowed == newValue) { + return + } + + pushManager.setDeviceNotificationsAllowed(!isAllowed) + + // when using FCM + // need to register on servers + if (isConnected && pushManager.useFcm() && (pushManager.isServerRegistered || pushManager.isServerUnRegistered)) { + val listener = object : ApiCallback { + + private fun onDone() { + activity?.runOnUiThread { + hideLoadingView(true) + refreshPushersList() + } + } + + override fun onSuccess(info: Void?) { + onDone() + } + + override fun onMatrixError(e: MatrixError?) { + // Set again the previous state + pushManager.setDeviceNotificationsAllowed(isAllowed) + onDone() + } + + override fun onNetworkError(e: java.lang.Exception?) { + // Set again the previous state + pushManager.setDeviceNotificationsAllowed(isAllowed) + onDone() + } + + override fun onUnexpectedError(e: java.lang.Exception?) { + // Set again the previous state + pushManager.setDeviceNotificationsAllowed(isAllowed) + onDone() + } + } + + displayLoadingView() + if (pushManager.isServerRegistered) { + pushManager.unregister(listener) + } else { + pushManager.register(listener) + } + } + } + + // check if there is an update + + // on some old android APIs, + // the callback is called even if there is no user interaction + // so the value will be checked to ensure there is really no update. + else -> { + + val ruleId = mPrefKeyToBingRuleId[preferenceKey] + val rule = mSession.dataHandler.pushRules()?.findDefaultRule(ruleId) + + // check if there is an update + var curValue = null != rule && rule.isEnabled + + if (TextUtils.equals(ruleId, BingRule.RULE_ID_DISABLE_ALL) || TextUtils.equals(ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) { + curValue = !curValue + } + + // on some old android APIs, + // the callback is called even if there is no user interaction + // so the value will be checked to ensure there is really no update. + if (newValue == curValue) { + return + } + + if (null != rule) { + displayLoadingView() + mSession.dataHandler.bingRulesManager.updateEnableRuleStatus(rule, !rule.isEnabled, object : BingRulesManager.onBingRuleUpdateListener { + private fun onDone() { + refreshDisplay() + hideLoadingView() + } + + override fun onBingRuleUpdateSuccess() { + onDone() + } + + override fun onBingRuleUpdateFailure(errorMessage: String) { + activity?.toast(errorMessage) + onDone() + } + }) + } + } + } + */ + } + + /** + * Update the displayname. + */ + private fun onDisplayNameClick(value: String?) { + /* TODO + if (!TextUtils.equals(mSession.myUser.displayname, value)) { + displayLoadingView() + + mSession.myUser.updateDisplayName(value, object : ApiCallback { + override fun onSuccess(info: Void?) { + // refresh the settings value + PreferenceManager.getDefaultSharedPreferences(activity).edit { + putString(PreferencesManager.SETTINGS_DISPLAY_NAME_PREFERENCE_KEY, value) + } + + onCommonDone(null) + + refreshDisplay() + } + + override fun onNetworkError(e: Exception) { + onCommonDone(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + if (MatrixError.M_CONSENT_NOT_GIVEN == e.errcode) { + activity?.runOnUiThread { + hideLoadingView() + (activity as VectorAppCompatActivity).consentNotGivenHelper.displayDialog(e) + } + } else { + onCommonDone(e.localizedMessage) + } + } + + override fun onUnexpectedError(e: Exception) { + onCommonDone(e.localizedMessage) + } + }) + } + */ + } + + private fun displayRingtonePicker() { + val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { + putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, getString(R.string.settings_call_ringtone_dialog_title)) + putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false) + putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) + putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE) + activity?.let { putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, getCallRingtoneUri(it)) } + } + startActivityForResult(intent, REQUEST_CALL_RINGTONE) + } + + /** + * Update the avatar. + */ + private fun onUpdateAvatarClick() { + /* TODO + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { + changeAvatar() + } + */ + } + + private fun changeAvatar() { + /* TODO + val intent = Intent(activity, VectorMediaPickerActivity::class.java) + intent.putExtra(VectorMediaPickerActivity.EXTRA_AVATAR_MODE, true) + startActivityForResult(intent, VectorUtils.TAKE_IMAGE) + */ + } + + /** + * Refresh the notification privacy setting + */ + private fun refreshNotificationPrivacy() { + /* TODO + val pushManager = Matrix.getInstance(activity).pushManager + + // this setting apply only with FCM for the moment + if (pushManager.useFcm()) { + val notificationPrivacyString = NotificationPrivacyActivity.getNotificationPrivacyString(activity, + pushManager.notificationPrivacy) + mNotificationPrivacyPreference.summary = notificationPrivacyString + } else { + notificationsSettingsCategory.removePreference(mNotificationPrivacyPreference) + } + */ + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (resultCode == Activity.RESULT_OK) { + when (requestCode) { + REQUEST_CALL_RINGTONE -> { + val callRingtoneUri: Uri? = data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) + val thisActivity = activity + if (callRingtoneUri != null && thisActivity != null) { + setCallRingtoneUri(thisActivity, callRingtoneUri) + mCallRingtonePreference.summary = getCallRingtoneName(thisActivity) + } + } + REQUEST_E2E_FILE_REQUEST_CODE -> importKeys(data) + REQUEST_NEW_PHONE_NUMBER -> refreshPhoneNumbersList() + REQUEST_PHONEBOOK_COUNTRY -> onPhonebookCountryUpdate(data) + REQUEST_LOCALE -> { + activity?.let { + startActivity(it.intent) + it.finish() + } + } + /* TODO + VectorUtils.TAKE_IMAGE -> { + val thumbnailUri = VectorUtils.getThumbnailUriFromIntent(activity, data, mSession.mediaCache) + + if (null != thumbnailUri) { + displayLoadingView() + + val resource = ResourceUtils.openResource(activity, thumbnailUri, null) + + if (null != resource) { + mSession.mediaCache.uploadContent(resource.mContentStream, null, resource.mMimeType, null, object : MXMediaUploadListener() { + + override fun onUploadError(uploadId: String?, serverResponseCode: Int, serverErrorMessage: String?) { + activity?.runOnUiThread { onCommonDone(serverResponseCode.toString() + " : " + serverErrorMessage) } + } + + override fun onUploadComplete(uploadId: String?, contentUri: String?) { + activity?.runOnUiThread { + mSession.myUser.updateAvatarUrl(contentUri, object : ApiCallback { + override fun onSuccess(info: Void?) { + onCommonDone(null) + refreshDisplay() + } + + override fun onNetworkError(e: Exception) { + onCommonDone(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + if (MatrixError.M_CONSENT_NOT_GIVEN == e.errcode) { + activity?.runOnUiThread { + hideLoadingView() + (activity as VectorAppCompatActivity).consentNotGivenHelper.displayDialog(e) + } + } else { + onCommonDone(e.localizedMessage) + } + } + + override fun onUnexpectedError(e: Exception) { + onCommonDone(e.localizedMessage) + } + }) + } + } + }) + } + } + } + */ + } + } + } + + /** + * Refresh the known information about the account + */ + private fun refreshPreferences() { + PreferenceManager.getDefaultSharedPreferences(activity).edit { + putString(PreferencesManager.SETTINGS_DISPLAY_NAME_PREFERENCE_KEY, "TODO") //mSession.myUser.displayname) + putString(PreferencesManager.SETTINGS_VERSION_PREFERENCE_KEY, "TODO") // VectorUtils.getApplicationVersion(activity)) + + /* TODO + mSession.dataHandler.pushRules()?.let { + for (preferenceKey in mPrefKeyToBingRuleId.keys) { + val preference = findPreference(preferenceKey) + + if (null != preference && preference is SwitchPreference) { + val ruleId = mPrefKeyToBingRuleId[preferenceKey] + + val rule = it.findDefaultRule(ruleId) + var isEnabled = null != rule && rule.isEnabled + + if (TextUtils.equals(ruleId, BingRule.RULE_ID_DISABLE_ALL) || TextUtils.equals(ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) { + isEnabled = !isEnabled + } else if (isEnabled) { + val actions = rule?.actions + + // no action -> noting will be done + if (null == actions || actions.isEmpty()) { + isEnabled = false + } else if (1 == actions.size) { + try { + isEnabled = !TextUtils.equals(actions[0] as String, BingRule.ACTION_DONT_NOTIFY) + } catch (e: Exception) { + Timber.e(e, "## refreshPreferences failed " + e.message) + } + + } + }// check if the rule is only defined by don't notify + + putBoolean(preferenceKey, isEnabled) + } + } + } + */ + } + } + + /** + * Display a dialog which asks confirmation for the deletion of a 3pid + * + * @param pid the 3pid to delete + * @param preferenceSummary the displayed 3pid + */ + private fun displayDelete3PIDConfirmationDialog(/* TODO pid: ThirdPartyIdentifier,*/ preferenceSummary: CharSequence) { + val mediumFriendlyName = "TODO" // ThreePid.getMediumFriendlyName(pid.medium, activity).toLowerCase(VectorLocale.applicationLocale) + val dialogMessage = getString(R.string.settings_delete_threepid_confirmation, mediumFriendlyName, preferenceSummary) + + activity?.let { + AlertDialog.Builder(it) + .setTitle(R.string.dialog_title_confirmation) + .setMessage(dialogMessage) + .setPositiveButton(R.string.remove) { _, _ -> + /* TODO + displayLoadingView() + + mSession.myUser.delete3Pid(pid, object : ApiCallback { + override fun onSuccess(info: Void?) { + when (pid.medium) { + ThreePid.MEDIUM_EMAIL -> refreshEmailsList() + ThreePid.MEDIUM_MSISDN -> refreshPhoneNumbersList() + } + onCommonDone(null) + } + + override fun onNetworkError(e: Exception) { + onCommonDone(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + onCommonDone(e.localizedMessage) + } + + override fun onUnexpectedError(e: Exception) { + onCommonDone(e.localizedMessage) + } + }) + */ + } + .setNegativeButton(R.string.cancel, null) + .show() + } + } + + //============================================================================================================== + // ignored users list management + //============================================================================================================== + + /** + * Refresh the ignored users list + */ + private fun refreshIgnoredUsersList() { + val ignoredUsersList = mutableListOf() // TODO mSession.dataHandler.ignoredUserIds + + ignoredUsersList.sortWith(Comparator { u1, u2 -> + u1.toLowerCase(VectorLocale.applicationLocale).compareTo(u2.toLowerCase(VectorLocale.applicationLocale)) + }) + + val preferenceScreen = preferenceScreen + + preferenceScreen.removePreference(mIgnoredUserSettingsCategory) + preferenceScreen.removePreference(mIgnoredUserSettingsCategoryDivider) + mIgnoredUserSettingsCategory.removeAll() + + if (ignoredUsersList.size > 0) { + preferenceScreen.addPreference(mIgnoredUserSettingsCategoryDivider) + preferenceScreen.addPreference(mIgnoredUserSettingsCategory) + + for (userId in ignoredUsersList) { + val preference = Preference(activity) + + preference.title = userId + preference.key = IGNORED_USER_KEY_BASE + userId + + preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + activity?.let { + AlertDialog.Builder(it) + .setMessage(getString(R.string.settings_unignore_user, userId)) + .setPositiveButton(R.string.yes) { _, _ -> + displayLoadingView() + + val idsList = ArrayList() + idsList.add(userId) + + /* TODO + mSession.unIgnoreUsers(idsList, object : ApiCallback { + override fun onSuccess(info: Void?) { + onCommonDone(null) + } + + override fun onNetworkError(e: Exception) { + onCommonDone(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + onCommonDone(e.localizedMessage) + } + + override fun onUnexpectedError(e: Exception) { + onCommonDone(e.localizedMessage) + } + }) + */ + } + .setNegativeButton(R.string.no, null) + .show() + } + + false + } + + mIgnoredUserSettingsCategory.addPreference(preference) + } + } + } + + //============================================================================================================== + // pushers list management + //============================================================================================================== + + /** + * Refresh the pushers list + */ + private fun refreshPushersList() { + activity?.let { activity -> + /* TODO + val pushManager = Matrix.getInstance(activity).pushManager + val pushersList = ArrayList(pushManager.mPushersList) + + if (pushersList.isEmpty()) { + preferenceScreen.removePreference(mPushersSettingsCategory) + preferenceScreen.removePreference(mPushersSettingsDivider) + return + } + + // check first if there is an update + var isNewList = true + if (pushersList.size == mDisplayedPushers.size) { + isNewList = !mDisplayedPushers.containsAll(pushersList) + } + + if (isNewList) { + // remove the displayed one + mPushersSettingsCategory.removeAll() + + // add new emails list + mDisplayedPushers = pushersList + + var index = 0 + + for (pusher in mDisplayedPushers) { + if (null != pusher.lang) { + val isThisDeviceTarget = TextUtils.equals(pushManager.currentRegistrationToken, pusher.pushkey) + + val preference = VectorPreference(activity).apply { + mTypeface = if (isThisDeviceTarget) Typeface.BOLD else Typeface.NORMAL + } + preference.title = pusher.deviceDisplayName + preference.summary = pusher.appDisplayName + preference.key = PUSHER_PREFERENCE_KEY_BASE + index + index++ + mPushersSettingsCategory.addPreference(preference) + + // the user cannot remove the self device target + if (!isThisDeviceTarget) { + preference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener { + override fun onPreferenceLongClick(preference: Preference): Boolean { + AlertDialog.Builder(activity) + .setTitle(R.string.dialog_title_confirmation) + .setMessage(R.string.settings_delete_notification_targets_confirmation) + .setPositiveButton(R.string.remove) + { _, _ -> + displayLoadingView() + pushManager.unregister(mSession, pusher, object : ApiCallback { + override fun onSuccess(info: Void?) { + refreshPushersList() + onCommonDone(null) + } + + override fun onNetworkError(e: Exception) { + onCommonDone(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + onCommonDone(e.localizedMessage) + } + + override fun onUnexpectedError(e: Exception) { + onCommonDone(e.localizedMessage) + } + }) + } + .setNegativeButton(R.string.cancel, null) + .show() + return true + } + } + } + } + } + } + */ + } + } + + //============================================================================================================== + // Email management + //============================================================================================================== + + /** + * Refresh the emails list + */ + private fun refreshEmailsList() { + val currentEmail3PID = emptyList() // TODO ArrayList(mSession.myUser.getlinkedEmails()) + + val newEmailsList = ArrayList() + for (identifier in currentEmail3PID) { + // TODO newEmailsList.add(identifier.address) + } + + // check first if there is an update + var isNewList = true + if (newEmailsList.size == mDisplayedEmails.size) { + isNewList = !mDisplayedEmails.containsAll(newEmailsList) + } + + if (isNewList) { + // remove the displayed one + run { + var index = 0 + while (true) { + val preference = mUserSettingsCategory.findPreference(EMAIL_PREFERENCE_KEY_BASE + index) + + if (null != preference) { + mUserSettingsCategory.removePreference(preference) + } else { + break + } + index++ + } + } + + // add new emails list + mDisplayedEmails = newEmailsList + + val addEmailBtn = mUserSettingsCategory.findPreference(ADD_EMAIL_PREFERENCE_KEY) + ?: return + + var order = addEmailBtn.order + + for ((index, email3PID) in currentEmail3PID.withIndex()) { + val preference = VectorPreference(activity!!) + + preference.title = getString(R.string.settings_email_address) + preference.summary = "TODO" // email3PID.address + preference.key = EMAIL_PREFERENCE_KEY_BASE + index + preference.order = order + + preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { pref -> + displayDelete3PIDConfirmationDialog(/* TODO email3PID, */ pref.summary) + true + } + + preference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener { + override fun onPreferenceLongClick(preference: Preference): Boolean { + activity?.let { copyToClipboard(it, "TODO") } //email3PID.address) } + return true + } + } + + mUserSettingsCategory.addPreference(preference) + + order++ + } + + addEmailBtn.order = order + } + } + + /** + * A request has been processed. + * Display a toast if there is a an error message + * + * @param errorMessage the error message + */ + private fun onCommonDone(errorMessage: String?) { + activity?.runOnUiThread { + if (!TextUtils.isEmpty(errorMessage) && errorMessage != null) { + activity?.toast(errorMessage!!) + } + hideLoadingView() + } + } + + /** + * Attempt to add a new email to the account + * + * @param email the email to add. + */ + private fun addEmail(email: String) { + // check first if the email syntax is valid + // if email is null , then also its invalid email + if (TextUtils.isEmpty(email) || !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) { + activity?.toast(R.string.auth_invalid_email) + return + } + + // check first if the email syntax is valid + if (mDisplayedEmails.indexOf(email) >= 0) { + activity?.toast(R.string.auth_email_already_defined) + return + } + + /* TODO + val pid = ThreePid(email, ThreePid.MEDIUM_EMAIL) + + displayLoadingView() + + mSession.myUser.requestEmailValidationToken(pid, object : ApiCallback { + override fun onSuccess(info: Void?) { + activity?.runOnUiThread { showEmailValidationDialog(pid) } + } + + override fun onNetworkError(e: Exception) { + onCommonDone(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + if (TextUtils.equals(MatrixError.THREEPID_IN_USE, e.errcode)) { + onCommonDone(getString(R.string.account_email_already_used_error)) + } else { + onCommonDone(e.localizedMessage) + } + } + + override fun onUnexpectedError(e: Exception) { + onCommonDone(e.localizedMessage) + } + }) + */ + } + + /** + * Show an email validation dialog to warn the user tho valid his email link. + * + * @param pid the used pid. + */ + /* TODO + private fun showEmailValidationDialog(pid: ThreePid) { + activity?.let { + AlertDialog.Builder(it) + .setTitle(R.string.account_email_validation_title) + .setMessage(R.string.account_email_validation_message) + .setPositiveButton(R.string._continue) { _, _ -> + mSession.myUser.add3Pid(pid, true, object : ApiCallback { + override fun onSuccess(info: Void?) { + it.runOnUiThread { + hideLoadingView() + refreshEmailsList() + } + } + + override fun onNetworkError(e: Exception) { + onCommonDone(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + if (TextUtils.equals(e.errcode, MatrixError.THREEPID_AUTH_FAILED)) { + it.runOnUiThread { + hideLoadingView() + it.toast(R.string.account_email_validation_error) + } + } else { + onCommonDone(e.localizedMessage) + } + } + + override fun onUnexpectedError(e: Exception) { + onCommonDone(e.localizedMessage) + } + }) + } + .setNegativeButton(R.string.cancel) { _, _ -> + hideLoadingView() + } + .show() + } + } + */ + + //============================================================================================================== + // Phone number management + //============================================================================================================== + + /** + * Refresh phone number list + */ + private fun refreshPhoneNumbersList() { + /* TODO + val currentPhoneNumber3PID = ArrayList(mSession.myUser.getlinkedPhoneNumbers()) + + val phoneNumberList = ArrayList() + for (identifier in currentPhoneNumber3PID) { + phoneNumberList.add(identifier.address) + } + + // check first if there is an update + var isNewList = true + if (phoneNumberList.size == mDisplayedPhoneNumber.size) { + isNewList = !mDisplayedPhoneNumber.containsAll(phoneNumberList) + } + + if (isNewList) { + // remove the displayed one + run { + var index = 0 + while (true) { + val preference = mUserSettingsCategory.findPreference(PHONE_NUMBER_PREFERENCE_KEY_BASE + index) + + if (null != preference) { + mUserSettingsCategory.removePreference(preference) + } else { + break + } + index++ + } + } + + // add new phone number list + mDisplayedPhoneNumber = phoneNumberList + + val addPhoneBtn = mUserSettingsCategory.findPreference(ADD_PHONE_NUMBER_PREFERENCE_KEY) + ?: return + + var order = addPhoneBtn.order + + for ((index, phoneNumber3PID) in currentPhoneNumber3PID.withIndex()) { + val preference = VectorPreference(activity!!) + + preference.title = getString(R.string.settings_phone_number) + var phoneNumberFormatted = phoneNumber3PID.address + try { + // Attempt to format phone number + val phoneNumber = PhoneNumberUtil.getInstance().parse("+$phoneNumberFormatted", null) + phoneNumberFormatted = PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL) + } catch (e: NumberParseException) { + // Do nothing, we will display raw version + } + + preference.summary = phoneNumberFormatted + preference.key = PHONE_NUMBER_PREFERENCE_KEY_BASE + index + preference.order = order + + preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + displayDelete3PIDConfirmationDialog(phoneNumber3PID, preference.summary) + true + } + + preference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener { + override fun onPreferenceLongClick(preference: Preference): Boolean { + activity?.let { copyToClipboard(it, phoneNumber3PID.address) } + return true + } + } + + order++ + mUserSettingsCategory.addPreference(preference) + } + + addPhoneBtn.order = order + } + */ + } + + //============================================================================================================== + // contacts management + //============================================================================================================== + + private fun setContactsPreferences() { + /* TODO + // Permission + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // on Android >= 23, use the system one + mContactSettingsCategory.removePreference(findPreference(ContactsManager.CONTACTS_BOOK_ACCESS_KEY)) + } + // Phonebook country + mContactPhonebookCountryPreference.summary = PhoneNumberUtils.getHumanCountryCode(PhoneNumberUtils.getCountryCode(activity)) + + mContactPhonebookCountryPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + val intent = CountryPickerActivity.getIntent(activity, true) + startActivityForResult(intent, REQUEST_PHONEBOOK_COUNTRY) + true + } + */ + } + + private fun onPhonebookCountryUpdate(data: Intent?) { + /* TODO + if (data != null && data.hasExtra(CountryPickerActivity.EXTRA_OUT_COUNTRY_NAME) + && data.hasExtra(CountryPickerActivity.EXTRA_OUT_COUNTRY_CODE)) { + val countryCode = data.getStringExtra(CountryPickerActivity.EXTRA_OUT_COUNTRY_CODE) + if (!TextUtils.equals(countryCode, PhoneNumberUtils.getCountryCode(activity))) { + PhoneNumberUtils.setCountryCode(activity, countryCode) + mContactPhonebookCountryPreference.summary = data.getStringExtra(CountryPickerActivity.EXTRA_OUT_COUNTRY_NAME) + } + } + */ + } + + //============================================================================================================== + // user interface management + //============================================================================================================== + + private fun setUserInterfacePreferences() { + // Selected language + selectedLanguagePreference.summary = VectorLocale.localeToLocalisedString(VectorLocale.applicationLocale) + + selectedLanguagePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + // TODO startActivityForResult(LanguagePickerActivity.getIntent(activity), REQUEST_LOCALE) + true + } + + // Text size + textSizePreference.summary = FontScale.getFontScaleDescription(activity!!) + + textSizePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + activity?.let { displayTextSizeSelection(it) } + true + } + } + + private fun displayTextSizeSelection(activity: Activity) { + val inflater = activity.layoutInflater + val layout = inflater.inflate(R.layout.dialog_select_text_size, null) + + val dialog = AlertDialog.Builder(activity) + .setTitle(R.string.font_size) + .setView(layout) + .setPositiveButton(R.string.ok, null) + .setNegativeButton(R.string.cancel, null) + .show() + + val linearLayout = layout.findViewById(R.id.text_selection_group_view) + + val childCount = linearLayout.childCount + + val scaleText = FontScale.getFontScaleDescription(activity) + + for (i in 0 until childCount) { + val v = linearLayout.getChildAt(i) + + if (v is CheckedTextView) { + v.isChecked = TextUtils.equals(v.text, scaleText) + + v.setOnClickListener { + dialog.dismiss() + FontScale.updateFontScale(activity, v.text.toString()) + activity.startActivity(activity.intent) + activity.finish() + } + } + } + } + + //============================================================================================================== + // background sync management + //============================================================================================================== + + /** + * Convert a delay in seconds to string + * + * @param seconds the delay in seconds + * @return the text + */ + private fun secondsToText(seconds: Int): String { + return if (seconds > 1) { + seconds.toString() + " " + getString(R.string.settings_seconds) + } else { + seconds.toString() + " " + getString(R.string.settings_second) + } + } + + /** + * Refresh the background sync preference + */ + private fun refreshBackgroundSyncPrefs() { + /* TODO + activity?.let { activity -> + val pushManager = Matrix.getInstance(activity).pushManager + + val timeout = pushManager.backgroundSyncTimeOut / 1000 + val delay = pushManager.backgroundSyncDelay / 1000 + + // update the settings + PreferenceManager.getDefaultSharedPreferences(activity).edit { + putString(PreferencesManager.SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, timeout.toString() + "") + putString(PreferencesManager.SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, delay.toString() + "") + } + + mSyncRequestTimeoutPreference?.let { + it.summary = secondsToText(timeout) + it.text = timeout.toString() + "" + + it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + var newTimeOut = timeout + + try { + newTimeOut = Integer.parseInt(newValue as String) + } catch (e: Exception) { + Timber.e(e, "## refreshBackgroundSyncPrefs : parseInt failed " + e.message) + } + + if (newTimeOut != timeout) { + pushManager.backgroundSyncTimeOut = newTimeOut * 1000 + + activity.runOnUiThread { refreshBackgroundSyncPrefs() } + } + + false + } + } + + mSyncRequestDelayPreference?.let { + it.summary = secondsToText(delay) + it.text = delay.toString() + "" + + it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + var newDelay = delay + + try { + newDelay = Integer.parseInt(newValue as String) + } catch (e: Exception) { + Timber.e(e, "## refreshBackgroundSyncPrefs : parseInt failed " + e.message) + } + + if (newDelay != delay) { + pushManager.backgroundSyncDelay = newDelay * 1000 + + activity.runOnUiThread { refreshBackgroundSyncPrefs() } + } + + false + } + } + } + */ + } + + //============================================================================================================== + // Cryptography + //============================================================================================================== + + private fun removeCryptographyPreference() { + preferenceScreen.let { + it.removePreference(mCryptographyCategory) + it.removePreference(mCryptographyCategoryDivider) + + // Also remove keys management section + it.removePreference(mCryptographyManageCategory) + it.removePreference(mCryptographyManageCategoryDivider) + } + } + + /** + * Build the cryptography preference section. + * + * @param aMyDeviceInfo the device info + */ + private fun refreshCryptographyPreference(aMyDeviceInfo: DeviceInfo?) { + val userId = mSession.sessionParams.credentials.userId + val deviceId = mSession.sessionParams.credentials.deviceId + + // device name + if (null != aMyDeviceInfo) { + cryptoInfoDeviceNamePreference.summary = "TODO" // aMyDeviceInfo.display_name + + cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + displayDeviceRenameDialog(aMyDeviceInfo) + true + } + + cryptoInfoDeviceNamePreference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener { + override fun onPreferenceLongClick(preference: Preference): Boolean { + activity?.let { copyToClipboard(it, "TODO") } //aMyDeviceInfo.display_name) } + return true + } + } + } + + // crypto section: device ID + if (!TextUtils.isEmpty(deviceId)) { + cryptoInfoDeviceIdPreference.summary = deviceId + + cryptoInfoDeviceIdPreference.setOnPreferenceClickListener { + activity?.let { copyToClipboard(it, deviceId!!) } + true + } + + + manageBackupPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + context?.let { + // TODO startActivity(KeysBackupManageActivity.intent(it, mSession.myUserId)) + } + false + } + + exportPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + exportKeys() + true + } + + importPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + importKeys() + true + } + } + + // crypto section: device key (fingerprint) + if (!TextUtils.isEmpty(deviceId) && !TextUtils.isEmpty(userId)) { + /* TODO + mSession.crypto?.getDeviceInfo(userId, deviceId, object : SimpleApiCallback() { + override fun onSuccess(deviceInfo: MXDeviceInfo?) { + if (null != deviceInfo && !TextUtils.isEmpty(deviceInfo.fingerprint()) && null != activity) { + cryptoInfoTextPreference.summary = deviceInfo.getFingerprintHumanReadable() + + cryptoInfoTextPreference.setOnPreferenceClickListener { + activity?.let { copyToClipboard(it, deviceInfo.fingerprint()) } + true + } + } + } + }) + */ + } + + sendToUnverifiedDevicesPref.isChecked = false + + /* TODO + mSession.crypto?.getGlobalBlacklistUnverifiedDevices(object : SimpleApiCallback() { + override fun onSuccess(status: Boolean) { + sendToUnverifiedDevicesPref.isChecked = status + } + }) + + sendToUnverifiedDevicesPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + mSession.crypto?.getGlobalBlacklistUnverifiedDevices(object : SimpleApiCallback() { + override fun onSuccess(status: Boolean) { + if (sendToUnverifiedDevicesPref.isChecked != status) { + mSession.crypto + ?.setGlobalBlacklistUnverifiedDevices(sendToUnverifiedDevicesPref.isChecked, object : SimpleApiCallback() { + override fun onSuccess(info: Void?) { + + } + }) + } + } + }) + + true + } + */ + } + + //============================================================================================================== + // devices list + //============================================================================================================== + + private fun removeDevicesPreference() { + preferenceScreen.let { + it.removePreference(mDevicesListSettingsCategory) + it.removePreference(mDevicesListSettingsCategoryDivider) + } + } + + /** + * Force the refresh of the devices list.

+ * The devices list is the list of the devices where the user as looged in. + * It can be any mobile device, as any browser. + */ + private fun refreshDevicesList() { + if (mSession.isCryptoEnabled() && !TextUtils.isEmpty(mSession.sessionParams.credentials.deviceId)) { + // display a spinner while loading the devices list + if (0 == mDevicesListSettingsCategory.preferenceCount) { + activity?.let { + val preference = ProgressBarPreference(it) + mDevicesListSettingsCategory.addPreference(preference) + } + } + + /* TODO + mSession.getDevicesList(object : ApiCallback { + override fun onSuccess(info: DevicesListResponse) { + if (info.devices.isEmpty()) { + removeDevicesPreference() + } else { + buildDevicesSettings(info.devices) + } + } + + override fun onNetworkError(e: Exception) { + removeDevicesPreference() + onCommonDone(e.message) + } + + override fun onMatrixError(e: MatrixError) { + removeDevicesPreference() + onCommonDone(e.message) + } + + override fun onUnexpectedError(e: Exception) { + removeDevicesPreference() + onCommonDone(e.message) + } + }) + */ + } else { + removeDevicesPreference() + removeCryptographyPreference() + } + } + + /** + * Build the devices portion of the settings.

+ * Each row correspond to a device ID and its corresponding device name. Clicking on the row + * display a dialog containing: the device ID, the device name and the "last seen" information. + * + * @param aDeviceInfoList the list of the devices + */ + private fun buildDevicesSettings(aDeviceInfoList: List) { + var preference: VectorPreference + var typeFaceHighlight: Int + var isNewList = true + val myDeviceId = mSession.sessionParams.credentials.deviceId + + /* TODO + if (aDeviceInfoList.size == mDevicesNameList.size) { + isNewList = !mDevicesNameList.containsAll(aDeviceInfoList) + } + + if (isNewList) { + var prefIndex = 0 + mDevicesNameList = aDeviceInfoList + + // sort before display: most recent first + DeviceInfo.sortByLastSeen(mDevicesNameList) + + // start from scratch: remove the displayed ones + mDevicesListSettingsCategory.removeAll() + + for (deviceInfo in mDevicesNameList) { + // set bold to distinguish current device ID + if (null != myDeviceId && myDeviceId == deviceInfo.device_id) { + mMyDeviceInfo = deviceInfo + typeFaceHighlight = Typeface.BOLD + } else { + typeFaceHighlight = Typeface.NORMAL + } + + // add the edit text preference + preference = VectorPreference(activity!!).apply { + mTypeface = typeFaceHighlight + } + + if (null == deviceInfo.device_id && null == deviceInfo.display_name) { + continue + } else { + if (null != deviceInfo.device_id) { + preference.title = deviceInfo.device_id + } + + // display name parameter can be null (new JSON API) + if (null != deviceInfo.display_name) { + preference.summary = deviceInfo.display_name + } + } + + preference.key = DEVICES_PREFERENCE_KEY_BASE + prefIndex + prefIndex++ + + // onClick handler: display device details dialog + preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + displayDeviceDetailsDialog(deviceInfo) + true + } + + mDevicesListSettingsCategory.addPreference(preference) + } + + refreshCryptographyPreference(mMyDeviceInfo) + } + */ + } + + /** + * Display a dialog containing the device ID, the device name and the "last seen" information.<> + * This dialog allow to delete the corresponding device (see [.displayDeviceDeletionDialog]) + * + * @param aDeviceInfo the device information + */ + private fun displayDeviceDetailsDialog(aDeviceInfo: DeviceInfo) { + + activity?.let { + + val builder = AlertDialog.Builder(it) + val inflater = it.layoutInflater + val layout = inflater.inflate(R.layout.dialog_device_details, null) + var textView = layout.findViewById(R.id.device_id) + + textView.text = "TODO"//aDeviceInfo.device_id + + // device name + textView = layout.findViewById(R.id.device_name) + val displayName = "TODO" // if (TextUtils.isEmpty(aDeviceInfo.display_name)) LABEL_UNAVAILABLE_DATA else aDeviceInfo.display_name + textView.text = displayName + + // last seen info + textView = layout.findViewById(R.id.device_last_seen) + /* TODO + if (!TextUtils.isEmpty(aDeviceInfo.last_seen_ip)) { + val lastSeenIp = aDeviceInfo.last_seen_ip + val dateFormatTime = SimpleDateFormat("HH:mm:ss") + val time = dateFormatTime.format(Date(aDeviceInfo.last_seen_ts)) + val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()) + val lastSeenTime = dateFormat.format(Date(aDeviceInfo.last_seen_ts)) + ", " + time + val lastSeenInfo = getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime) + textView.text = lastSeenInfo + } else { + // hide last time seen section + layout.findViewById(R.id.device_last_seen_title).visibility = View.GONE + textView.visibility = View.GONE + } + */ + + // title & icon + builder.setTitle(R.string.devices_details_dialog_title) + .setIcon(android.R.drawable.ic_dialog_info) + .setView(layout) + .setPositiveButton(R.string.rename) { _, _ -> displayDeviceRenameDialog(aDeviceInfo) } + + /* TODO + // disable the deletion for our own device + if (!TextUtils.equals(mSession.crypto?.myDevice?.deviceId, aDeviceInfo.device_id)) { + builder.setNegativeButton(R.string.delete) { _, _ -> displayDeviceDeletionDialog(aDeviceInfo) } + } + */ + + builder.setNeutralButton(R.string.cancel, null) + .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + dialog.cancel() + return@OnKeyListener true + } + false + }) + .show() + } + } + + /** + * Display an alert dialog to rename a device + * + * @param aDeviceInfoToRename device info + */ + private fun displayDeviceRenameDialog(aDeviceInfoToRename: DeviceInfo) { + activity?.let { + val inflater = it.layoutInflater + val layout = inflater.inflate(R.layout.dialog_base_edit_text, null) + + val input = layout.findViewById(R.id.edit_text) + /* TODO + input.setText(aDeviceInfoToRename.display_name) + + AlertDialog.Builder(it) + .setTitle(R.string.devices_details_device_name) + .setView(layout) + .setPositiveButton(R.string.ok) { _, _ -> + displayLoadingView() + + val newName = input.text.toString() + + mSession.setDeviceName(aDeviceInfoToRename.device_id, newName, object : ApiCallback { + override fun onSuccess(info: Void?) { + hideLoadingView() + + // search which preference is updated + val count = mDevicesListSettingsCategory.preferenceCount + + for (i in 0 until count) { + val pref = mDevicesListSettingsCategory.getPreference(i) + + if (TextUtils.equals(aDeviceInfoToRename.device_id, pref.title)) { + pref.summary = newName + } + } + + // detect if the updated device is the current account one + if (TextUtils.equals(cryptoInfoDeviceIdPreference.summary, aDeviceInfoToRename.device_id)) { + cryptoInfoDeviceNamePreference.summary = newName + } + + // Also change the display name in aDeviceInfoToRename, in case of multiple renaming + aDeviceInfoToRename.display_name = newName + } + + override fun onNetworkError(e: Exception) { + onCommonDone(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + onCommonDone(e.localizedMessage) + } + + override fun onUnexpectedError(e: Exception) { + onCommonDone(e.localizedMessage) + } + }) + } + .setNegativeButton(R.string.cancel, null) + .show() + */ + } + } + + /** + * Try to delete a device. + * + * @param deviceId the device id + */ + private fun deleteDevice(deviceId: String) { + displayLoadingView() + /* TODO + mSession.deleteDevice(deviceId, mAccountPassword, object : ApiCallback { + override fun onSuccess(info: Void?) { + hideLoadingView() + refreshDevicesList() // force settings update + } + + private fun onError(message: String) { + mAccountPassword = null + onCommonDone(message) + } + + override fun onNetworkError(e: Exception) { + onError(e.localizedMessage) + } + + override fun onMatrixError(e: MatrixError) { + onError(e.localizedMessage) + } + + override fun onUnexpectedError(e: Exception) { + onError(e.localizedMessage) + } + }) + */ + } + + /** + * Display a delete confirmation dialog to remove a device.

+ * The user is invited to enter his password to confirm the deletion. + * + * @param aDeviceInfoToDelete device info + */ + private fun displayDeviceDeletionDialog(aDeviceInfoToDelete: DeviceInfo) { + /* + TODO + if (aDeviceInfoToDelete.device_id != null) { + if (!TextUtils.isEmpty(mAccountPassword)) { + deleteDevice(aDeviceInfoToDelete.device_id) + } else { + activity?.let { + val inflater = it.layoutInflater + val layout = inflater.inflate(R.layout.dialog_device_delete, null) + val passwordEditText = layout.findViewById(R.id.delete_password) + + AlertDialog.Builder(it) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.devices_delete_dialog_title) + .setView(layout) + .setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ -> + if (TextUtils.isEmpty(passwordEditText.toString())) { + it.toast(R.string.error_empty_field_your_password) + return@OnClickListener + } + mAccountPassword = passwordEditText.text.toString() + deleteDevice(aDeviceInfoToDelete.device_id) + }) + .setNegativeButton(R.string.cancel) { _, _ -> + hideLoadingView() + } + .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + dialog.cancel() + hideLoadingView() + return@OnKeyListener true + } + false + }) + .show() + } + } + } else { + Timber.e("## displayDeviceDeletionDialog(): sanity check failure") + } + */ + } + + /** + * Manage the e2e keys export. + */ + private fun exportKeys() { + // We need WRITE_EXTERNAL permission + /* + TODO + if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) { + activity?.let { activity -> + ExportKeysDialog().show(activity, object : ExportKeysDiaLog.ExportKeyDialogListener { + override fun onPassphrase(passphrase: String) { + displayLoadingView() + + CommonActivityUtils.exportKeys(mSession, passphrase, object : SimpleApiCallback(activity) { + override fun onSuccess(filename: String) { + hideLoadingView() + + AlertDialog.Builder(activity) + .setMessage(getString(R.string.encryption_export_saved_as, filename)) + .setCancelable(false) + .setPositiveButton(R.string.ok, null) + .show() + } + + override fun onNetworkError(e: Exception) { + super.onNetworkError(e) + hideLoadingView() + } + + override fun onMatrixError(e: MatrixError) { + super.onMatrixError(e) + hideLoadingView() + } + + override fun onUnexpectedError(e: Exception) { + super.onUnexpectedError(e) + hideLoadingView() + } + }) + } + }) + } + } + */ + } + + /** + * Manage the e2e keys import. + */ + @SuppressLint("NewApi") + private fun importKeys() { + // TODO activity?.let { openFileSelection(it, this, false, REQUEST_E2E_FILE_REQUEST_CODE) } + } + + /** + * Manage the e2e keys import. + * + * @param intent the intent result + */ + private fun importKeys(intent: Intent?) { + // sanity check + if (null == intent) { + return + } + + /* + TODO + val sharedDataItems = ArrayList(RoomMediaMessage.listRoomMediaMessages(intent)) + val thisActivity = activity + + if (sharedDataItems.isNotEmpty() && thisActivity != null) { + val sharedDataItem = sharedDataItems[0] + val dialogLayout = thisActivity.layoutInflater.inflate(R.layout.dialog_import_e2e_keys, null) + val builder = AlertDialog.Builder(thisActivity) + .setTitle(R.string.encryption_import_room_keys) + .setView(dialogLayout) + + val passPhraseEditText = dialogLayout.findViewById(R.id.dialog_e2e_keys_passphrase_edit_text) + val importButton = dialogLayout.findViewById