diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 1f055691..757cbec1 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -5,7 +5,7 @@ android:versionName="1.6.5"> + android:targetSdkVersion="25" /> + + @@ -42,8 +44,6 @@ android:enabled="true" > - - + 22 means we have to support the new permissions model + targetSdkVersion 25 //multiDexEnabled true //testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner" } @@ -57,7 +57,7 @@ android { minifyEnabled false useProguard false } - release { //keep on 'releae', set to 'all' when testing to make sure proguard is not deleting important stuff + release { //keep on 'release', set to 'all' when testing to make sure proguard is not deleting important stuff minifyEnabled true useProguard true proguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro' @@ -83,5 +83,6 @@ dependencies { androidTestCompile 'org.mockito:mockito-core:1.10.19' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.1'// Because mockito has some problems with dex environment androidTestCompile 'org.skyscreamer:jsonassert:1.3.0' + testCompile 'junit:junit:4.12' } diff --git a/res/layout/activity_refresh_list.xml b/res/layout/activity_refresh_list.xml new file mode 100644 index 00000000..bdc9f5ef --- /dev/null +++ b/res/layout/activity_refresh_list.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/res/menu/pairing.xml b/res/menu/pairing.xml index ffc8c144..1589a7ab 100644 --- a/res/menu/pairing.xml +++ b/res/menu/pairing.xml @@ -9,14 +9,6 @@ android:title="@string/refresh" /> - - - - diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml index 3d5c6bf7..8ac25088 100644 --- a/res/values-ar/strings.xml +++ b/res/values-ar/strings.xml @@ -5,13 +5,23 @@ تقرير البطّاريّة بلّغ عن حالة البطّاريّة دوريًّا اكشف نظام الملفّات + تسمح بتصفّح نظام ملفّات هذا الهاتف عن بعد مزامنة الحافظة شارك محتوى الحافظة - الدَّخل البعيد + الدّخل البعيد + استخدم الهاتف أو اللوحيّ كفأرة ولوحة مفاتيح + تحكّمات الوسائط المتعدّدة + توفّر تحكّمًا بعيدًا لمشغّل الوسائط + شغّل أمرًا + تحفّز أوامر عن بعد من الهاتف أو اللوحيّ وخزة أرسل واستقبل وخزات مزامنة الإخطارات انفذ إلى إخطاراتك من أجهزة أخرى + استقبل الإخطارات + استقبل الإخطارات من الجهاز الآخر واعرضها على أندرويد + شارك واستقبل + شارك الملفّات والعناوين بين الجهازين هذه الميزة غير متوفّرة في إصدار أندرويد لديك لا أجهزة حسنًا @@ -19,24 +29,33 @@ افتح الإعدادات عليك إعطاء التّطبيق صلاحيّات للنّفاذ إلى الإخطارات أرسل وخزة + تحكّمات الوسائط المتعدّدة + الدّخل البعيد + حرّك اصبعًا على الشّاشة لتحريك مؤشّر الفأرة. المس للنّقر واستخدم إصبعين أو ثلاث للزّرّين الأيمن والأوسط. انقر مطوّلًا للسّحب والإفلات. + اضبط إجراء اللمس بإصبعين + اضبط إجراء اللمس بثلاث أصابع + اضبط حساسيّة لوحة اللمس + اعكس اتّجاه التّمرير - - - Nothing + النّقر باليمين + النّقر بالوسط + لا شيء - Slowest - Above Slowest - Default - Above Default - Fastest + الأبطأ + الأقل بطئًا + الافتراضيّ + الأسرع قليلًا + الأسرع الأجهزة المقترن بها الأجهزة المتوفّرة الأجهزة المتذكَّرة فشل تحميل الملحقات (المس لمعلومات اكثر): - أزل الاقتران + إعدادات الملحقة + ألغِ الاقتران الجهاز المقترن غير قابل الوصول + اقرن جهازًا جديدًا جهاز مجهول الجهاز غير قابل الوصول طُلب الاقتران بالفعل @@ -46,14 +65,24 @@ ألغاه المستخدم ألغاه ندّ آخر استُقبل مفتاح غير صالح + معلومات التّعمية + لا يستخدم الجهاز الآخر إصدارة حديثة من «كدي المتّصل»، ستُستخدم طريقة التّعمية القديمة. + بصمة SHA1 لشهادة جهازك هي: + بصمة SHA1 لشهادة الجهاز البعيد هي: طُلب الاقتران طلب اقتران من %1s استُلمت وصلة من %1s المس لفتح \'%1s\' ملفّ وارد من %1s %1s - استُلم ملفّ من %1s + يرسل ملفًّا إلى %1s + %1s + استُقبل ملفّ من %1s + فشل استقبال الملفّ من %1s المس لفتح \'%1s\' + أرسل ملفًّا إلى %1s + %1s + %1s المس للإجابة أعد الاتّصال أرسل نقرة باليمين @@ -74,6 +103,8 @@ التّالي المستوى إعدادات الوسائط المتعدّدة + زرّا التّقدّم والإرجاع + اضبط الوقت عند نقر زرّيّ التّقديم أو الإرجاع. 10 ثوان 20 ثانية @@ -92,9 +123,13 @@ اسم جهاز غير صالح استُقبل نصّ، حُفظ إلى الحافظة قائمة أجهزة مخصّصة + اقرن جهازًا جديدًا + ألغِ اقتران %s أضف أجهزة بميفاق الإنترنت م​إ إخطارات مزعجة اهتزّ وشغّل صوتًا عند استقبال ملفّ + مرشّح الإخطارات + ستُزامن الإخطارات من التّطبيقات المحدّدة. التّخزين الدّاخليّ كلّ الملفّات بطاقة SD ‏%d @@ -102,7 +137,26 @@ (للقراءة فقط) صور الكاميرا أضف مضيفًا/م​إ + اسم المضيف أو عنوان IP لم يُعثر على مشغّلات استخدم هذا الخيار فقط إن لم يُكتَشف جهازك آليًّا. أدخِل عنوان م​إ أو اسم المضيف أدناه والمس الزرّ لإضافته إلى القائمة. المس عنصرًا موجودًا لإزالته من القائمة. %1$s على %2$s + أرسل ملفّات + أجهزة «كدي المتّصل» + الأجهزة الأخرى التي تشغّل «كدي المتّصل» وعلى نفس الشّبكة ستظهر هنا. + اقتُرن الجهاز + أعد تسمية الجهاز + أعد التّسمية + أنعش + الجهاز المقترن هذا لا يمكن الوصول إليه. تأكّد من اتّصاله بنفس الشّبكة. + لا متصفّحات ملفّات مثبّتة. + أرسل SMS + أرسل رسائل نصّيّة من سطح المكتب + لا يدعم جهازك هذه الملحقة + جِد جهازي + جِد جهازي اللوحيّ + يرّن هذا الجهاز لتجده + وُجد + افتح + أغلق diff --git a/res/values-ast/strings.xml b/res/values-ast/strings.xml index a21a67b6..a35388af 100644 --- a/res/values-ast/strings.xml +++ b/res/values-ast/strings.xml @@ -1,33 +1,39 @@ - Notificador telefónicu + Avisador telefónicu Unvia avisos pa SMS y llamaes Informe de batería - Infoma davezu del estáu de la batería + Informe periódicu del estáu de la batería + Permite restolar remotamente a esti preséu Sincronización del cartafueyu Comparte\'l conteníu del cartafueyu Entrada remota - Controles remotos multimedia + Usa\'l to teléfonu o tableta como panel táutil y tecláu + Pulsaciones remotes + Controles multimedia + Forne un control remotu pal to reproductor multimedia + Execución de comandos + Aiciona comandos remotos del to teléfonu o tableta Ping - Unvia y recibi pings + Unvia y recibe pings Sincronización d\'avisos - Accede a los tos avisos dende otros preseos - Unvia y recibi pings + Accede a los tos avisos d\'otros preseos + Recibir avisos + Recibe avisos d\'otros preseos y amuésalos n\'Android + Compartir y recibir Comparte ficheros y URLs ente preseos Esta carauterística nun ta disponible na to versión d\'Android Ensin preseos Aceutar Encaboxar Abrir axustes - Necesites garantizar l\'accesu a los avisos + ¡ Unviar ping - Controles multimedia - Entrada remota - Muevi un deu enriba la pantalla pa mover el mur. calca pa un clic y usa dos/tres deos pa los motones de drecha y mediu. Usa un primíu llargu pa arrastrar y soltar. + Control multimedia - Clic drechu - Clic d\'en mediu - Nada + Right click + Middle click + Nothing Slowest @@ -36,101 +42,16 @@ Above Default Fastest - Preseos coneutaos - Preseos disponibles - Preseos recordaos - El preséu empareyáu nun ye agamable - Preséu desconocíu - Nun ye algamable\'l preséu - Empareyamientu yá solicitáu - El preséu yá ta empareyáu - Nun pudo unviase\'l paquete - Tiempu escosao - Encaboxáu pol usuariu - Encaboxáu pola otra parte - Recibióse una clave non válida - Solicitóse l\'empareyamientu - Solicitú d\'empareyamientu de %1s - Recibióse l\'enllaz de %1s - Calca p\'abrir «%1s» - Ficheru entrante de %1s - %1s - Unviando ficheru a %1s - %1s - Recibióse\'l ficheru de %1s - Fallu al recibir el ficheru de %1s - Tap to open \'%1s\' - Unvióse\'l ficheru a %1s - %1s - %1s - Calca pa responder - Reconeutar - Amosar tecláu - Preséu non empareyáu - Solicitar empareyamientu - Aceutar - Refugar - Preséu - Empareyar preséu - Control remotu - Axustes KDE Connect - Reproducir - Anterior - Rebobinar - Avance rápidu - Siguiente - Volume - Axustes multimedia - Botones d\'avanzar/rebobinar - Axusta\'l tiempu p\'avanzar/rebobinar al primise + Escosó\'l tiempu - 10 segundos - 20 segundos - 30 segundos - 1 minutu - 2 minutos + 10 seconds + 20 seconds + 30 seconds + 1 minute + 2 minutes - Esti preséu usa una versión vieya del protocolu - Esti preséu usa una versión anovada del protocolu - Axustes xenerales - Axustes - Axustes de %s - Nome de preséu - %s - Nome de preséu non válidu - Recibióse\'l testu y guardóse nel cartafueyu - Llista de preseos personalizada - Empareyar con un preséu nuevu - Desempareyar %s - Amestar preseos pola IP - Avisos sonoros - Fai vibrar y reproduz un soníu al recibir un ficheru - Peñera d\'avisos - Los avisos sincronizaránse coles aplicaciones esbillaes. - Almacenamientu internu - Tolos ficheros - Tarxeta SD %d - Tarxeta SD - (namai llectura) - Semeyes de cámara - Amestar agospiu/IP - Nome d\'agospiu o IP - Nun s\'alcontraron reproductores - Usa esta opción namái si\'l to preséu nun se deteuta automáticamente. Introduz la direición o\'l nome d\'agospiu y toca\'l botón p\'amestalu a la llista. Toca un elementu esistente pa desanicialu de la llista. - %1$s en %2$s - Unviar ficheros - Preseos KDE Connect - Deberíen apaecer equí otros preseos executando KDE Connect. - Preséu empareyáu - Renomar preséu - Renomar - Refrescar - Esti preséu empareyáu nun ye algamable. Asegúrate que ta coneutáu a la to mesma rede. - Nun hai restoladores de ficheros instalaos. - Unviar SMS + Nun hai restoladores de ficheros instalaos Unvia mensaxes de testu dende\'l to escritoriu - Esti complementu nun ta sofitáu pol preséu - Alcontrar - Abrir - Zarrar + Esti complementu nun lu sofita\'l preséu + Fai sonar el teléfonu pa qu\'asina pueas alcontralu diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml index dad48dd2..ec035e67 100644 --- a/res/values-bs/strings.xml +++ b/res/values-bs/strings.xml @@ -27,8 +27,6 @@ Srednji klik Ništa - desno - Srednje Slowest Above Slowest diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index 4c60746e..6312ea16 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -10,8 +10,8 @@ Comparteix el contingut del porta-retalls Entrada remota Usa el vostre telèfon o tauleta com un ratolí i un teclat - Rep les pulsacions de tecla remotes - Rep els esdeveniments de pulsacions de tecla des de dispositius remots + S\'estan rebent pulsacions de tecla remotes + S\'estan rebent esdeveniments de pulsacions de tecla des de dispositius remots Controls multimèdia Proporciona un comandament a distància pel reproductor multimèdia Executa una ordre @@ -33,7 +33,7 @@ Envia un ping Control multimèdia Fes servir les tecles remotes només en editar - No hi ha cap connexió activa amb el teclat remot, establiu-ne una al «kdeconnect» + No hi ha cap connexió activa amb el teclat remot, establiu-ne una al kde-connect La connexió amb el teclat remot està activa Hi ha més d\'una connexió amb un teclat remot, seleccioneu el dispositiu per configurar-lo Entrada remota @@ -47,9 +47,6 @@ Clic del mig No fer res - dret - mig - Predeterminada La més lenta Lenta @@ -176,4 +173,7 @@ L\'he trobat Obre Tanca + Us caldrà concedir permís per accedir a l\'emmagatzematge + Alguns connectors necessiten permisos per a funcionar (puntegeu per a més informació): + Aquest connector necessita permisos per a funcionar diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml index f5944c9b..f0c10f52 100644 --- a/res/values-cs/strings.xml +++ b/res/values-cs/strings.xml @@ -47,9 +47,6 @@ Kliknutí prostředním tlačítkem myši Nic - pravé - prostřední - výchozí Nejpomalejší Méně pomalý diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml index ca665a25..e49cef4b 100644 --- a/res/values-da/strings.xml +++ b/res/values-da/strings.xml @@ -47,15 +47,12 @@ Midterklik Intet - højre - midter - standard Mest langsom Over mest langsom Standard Over standard - Hurtigste + Hurtigst Forbundne enheder Tilgængelig enheder @@ -85,12 +82,15 @@ Indkommende fil fra %1s %1s Sender fil til %1s + Sender filer til %1s %1s + Sendte %1$d ud af %2$d filer Modtog fil fra %1s Kunne ikke modtage fil fra %1s Tap for at åbne \"%1s\" Fil sendt til %1s %1s + Kunne ikke sende filen til %1s %1s Tap for at svare Forbind igen @@ -137,6 +137,10 @@ Tilføj enheder via IP Støjende bekendtgørelser Vibrér og afspil en lyd når en fil modtages + Tilpas destinationsmappe + Modtagne filer vil dukke op i Downloads + Filer vil blive gemt i mappen nedenfor + Destinationsmappe Bekendtgørelsesfilter Bekendtgørelser vil blive synkroniseret for de valgte apps. Intern lagring diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index bd7d7587..b650ac64 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -13,7 +13,7 @@ Multimedia-Bedienung Eine Fernbedienung für Ihre Medienwiedergabe Befehl ausführen - Von Ihrem Telefon oder Tablett Befehle auf anderen Geräten ausführen + Von Ihrem Telefon oder Tablet Befehle auf anderen Geräten ausführen Ping Senden und Empfangen von Pings Benachrichtigungs-Abgleich @@ -41,9 +41,6 @@ Mittelklick Nichts - Rechts - Mitte - Standard Langsamste Langsam @@ -95,7 +92,7 @@ Mittelklick senden Tastatur anzeigen Das Gerät ist nicht verbunden - Verbindung angefordert + Verbindung anfordern Annehmen Ablehnen Gerät @@ -164,8 +161,8 @@ Text-Nachrichten von Ihrer Arbeitsfläche senden Dieses Modul wird durch das Gerät nicht unterstützt Mein Telefon suchen - Mein Tablett suchen - Ruft dieses Gerät an, um es zu suchen. + Mein Tablet suchen + Ruft dieses Gerät an, damit sie es finden können Gefunden Öffnen Schließen diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index 0f4e75f5..086b790c 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -41,9 +41,6 @@ Μεσαίο κλικ Τίποτα - δεξί - μεσαίο - προκαθορισμένο Το πιο αργό Πάνω από το πιο αργό diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml index de3f74d7..ccd6ff51 100644 --- a/res/values-en-rGB/strings.xml +++ b/res/values-en-rGB/strings.xml @@ -47,9 +47,6 @@ Middle click Nothing - right - middle - default Slowest Above Slowest diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 1240e079..987c62cd 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -11,7 +11,7 @@ Entrada remota Usar su teléfono o tableta como teclado y teclado táctil Recibir pulsaciones de teclas remotas - Reciba eventos de pulsación de teclas desde dispositivos remotos + Recibir eventos de pulsación de teclas desde dispositivos remotos Controles multimedia Proporciona un control remoto para su reproductor de medios Ejecutar orden @@ -47,9 +47,6 @@ Clic del botón central Nada - derecho - medio - por defecto Muy poco sensible Poco sensible @@ -176,4 +173,7 @@ Encontrado Abrir Cerrar + Debe otorgar permisos para acceder al almacenamiento + Algunos complementos necesitan permisos para funcionar (pulse para más información): + Este complemento necesita permisos para funcionar diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml index 18bd240d..849539a6 100644 --- a/res/values-et/strings.xml +++ b/res/values-et/strings.xml @@ -41,9 +41,6 @@ Keskklõps Ei tee midagi - parem - keskmine - vaikimisi Kõige aeglasem Kõige aeglasemast kiirem diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml index ac9b3e10..a70d95e3 100644 --- a/res/values-fi/strings.xml +++ b/res/values-fi/strings.xml @@ -41,9 +41,6 @@ Keskinapsautus Ei toimintoa - Oikea painike - Keskipainike - oletus Hitain Hitainta suurempi diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml index 5c83f175..28c97681 100644 --- a/res/values-gl/strings.xml +++ b/res/values-gl/strings.xml @@ -28,25 +28,25 @@ Mova un dedo na pantalla para mover o cursor. Toque para facer clic, e use dous ou tres dedos para os botóns secundario e central. Prema durante un tempo para arrastrar e soltar. Definir a acción de tocar con dous dedos Definir a acción de tocar con tres dedos + Definir a sensibilidade do punteiro táctil + Inverter a dirección de desprazamento Clic dereito Clic central Nada - dereita - medio - Slowest - Above Slowest - Default - Above Default - Fastest + O máis lento + Lento + Predeterminado + Rápido + O máis rápido Dispositivos conectados Dispositivos dispoñíbeis Dispositivos coñecidos Non foi posíbel cargar os seguintes complementos (toque para obter máis información): - Configuración do engadido + Configuración do complemento Desemparellarse O dispositivo emparellado está fóra do alcance. Emparellar cun novo dispositivo @@ -136,7 +136,7 @@ Outros dispositivos que estean a executar KDE Connect na mesma rede deberían aparecer aquí. Emparellouse co dispositivo Cambiar o nome do dispositivo - Mudar o nome + Cambiar o nome Actualizar Este dispositivo emparellado está fóra do alcance. Asegúrese de que está conectado á mesma rede. Non hai navegadores de ficheiros instalados. diff --git a/res/values-he/strings.xml b/res/values-he/strings.xml index 63c91c95..82726e88 100644 --- a/res/values-he/strings.xml +++ b/res/values-he/strings.xml @@ -5,15 +5,23 @@ דיווח סוללה מדווח על אחוז הסוללה למחשב גישה לקבצים - סנכנרון לוח העתקה - שתף בין המחשבים את מה שמועתק + אפשר להתקן המרוחק לדפדף בקבצים של הפלאפון מרחוק + סנכרון לוח העתקה + שתף בין המחשבים את כל מה שמועתק שליטה מרחוק + השתמש בפלאפון כדי לשלוט בעכבר ובמקלדת של ההתקן המרוחק + קבלת לחיצות מרחוק + קבלת אירועי מקלדת מהתקן מרוחק שליטה במדיה + מספק שליטה מרוחקת על נגן המדיה שלך הרץ פקודה + הרץ פקודה במחשב מהמכשיר שלך פינג שלח וקבל פינגים סנכרון התראות הראה את ההתראות מהפלאפון בהתקן אחר + קבלת התראות + קבל התראות מהתקן אחר והצג אותם במכשיר שלך שתף וקבל קבצים וכתובות שתף וקבל קבצים וכתובת אינטרנט בין התקנים אפשרות זו אינה זמינה בגרסת האנדרואיד שלך @@ -24,7 +32,11 @@ אתה צריך לתת הרשאות לגישה להתראות שלח פינג שליטה על המדיה - שלוט על המחשב + השתמש במקשים מרוחקים רק בעת עריכה + אין מקלדת מרוחקת מופעלת, הוסף אחת ל־kdeconnect + חיבור המקדלת המרוחקת פעיל + ישנם כמה מקלדות מרוחקות מחוברות, בחר את ההתקן להגדרה + שליטה מרחוק הזז את האצבע על המסך כדי להזיז את סמן העכבר במחשב. לחץ כדי ללחוץ במחשב, השתמש בשנים או שלוש אצבעות כדי ללחוץ על המקש הימני או האמצעי. השתמש בליחצה ארוכה לגרירה ושחרור. הגדר פעולה ללחיצת שתי אצבעות הגדר פעולה ללחיצת שלוש אצבעות @@ -35,9 +47,6 @@ לחיצה אצמעית (גלגלת) שום דבר - ימין - אמצע - ברירת מחדל הכי איטי יותר מההכי איטי @@ -59,9 +68,9 @@ ההתקן כבר מותאם לא יכול לשלוח חבילה נגמר הזמן - בוטל ע\"י המשתמש - בוטל ע\"י מישהו אחר - התקבל מפתח לא חוקי + בוטל על ידי המשתמש + בוטל על ידי מישהו אחר + התקבל מפתח לא תקין פרטי הצפנה ההתקן השני אינו משתמש בגרסה האחרונה של KDE Connect, משתמש בשיטת ההצפנה הישנה. טביעת האצבע SHA1 של ההתקן היא: @@ -69,16 +78,19 @@ בקשת התאמה בוקשה התאמה מ־%1s התקבל קישור מ־%1s - לחץ כדי לפתוח את \'%1s\' - התקבל קובץ ־%1s + לחץ כדי לפתוח את \"%1s\" + התקבל קובץ מאת %1s %1s - שולח קובץ ל־%1s + שולח קובץ אל %1s + שולח קובצים אל %1s %1s - התקבל קובץ מ־%1s - נכשל בקבלת קובץ מ־%1s - לחץ כדי לפתוח את %1s - הקובץ נשלח ל־%1s + "שולח %1$d מתוך %2$d קבצים " + התקבל קובץ מאת %1s + נכשל בקבלת קובץ מאת %1s + לחץ כדי לפתוח את \"%1s\" + הקובץ נשלח אל %1s %1s + נכשל בשליחת הקובץ אל %1s %1s לחץ כדי לענות התחבר מחדש @@ -109,22 +121,26 @@ דקה שתי דקות - שתף ל... + שתף אל... ההתקן משתמש בגרסה ישנה יותר ההתקן משתמש בגרסה חדשה יותר הגדרות כלליות הגדרות הגדרות %s - שם המכשיר + שם ההתקן %s - שם המכשיר לא תקין + שם ההתקן לא תקין התקבל טקסט, נשמר ללוח העתקה רשימת התקנים מותאמת אישית התאם התקן חדש בטל את ההתאמה עם %s - הוסף התקן ע\"י כתובת IP + הוסף התקן על ידי כתובת IP התראות רועשות רטוט ונגן צליל בעת קבלת קובץ + שנה תקיית יעד + קבצים שהתקבלו יהיו בהורדות + קבצים יאוכסנו בתיקיה למטה + תיקית יעד סנן התראות התראות יסונכרנו רק לאפליקציות נבחרות זיכרון פנימי @@ -133,11 +149,11 @@ כרטיס זיכרון (לקריאה בלבד) תמונות מצלמה - הוסף כתובת שרת או IP - כתבות שרת או IP + הוסף כתובת או IP + כתובת או IP לא נמצא נגן השתמש באפשרות זו רק אם המכשיר שלך לא מזוהה באופן אוטומטי. הקלד את כתובת הIP או את כינוי ההתקן למטה ולחץ על הכפתור כדי להוסיף לרשימה. לחץ על פריט קיים כדי להסיר אותו מהרשימה. - %1$s ב־%2$s + ‏%1$s אצל %2$s שלח קובץ מכשירי KDE Connect התקנים אחרים המריצים KDE Connect ברשת הנוכחית צריכים להופיע פה. @@ -145,12 +161,15 @@ שנה שם התקן שנה שם רענן - ההתקן המתואם לא זמין, וודא שהוא מחובר לאותה רשת אליה התקן זה מחובר. + ההתקן המתואם לא זמין, וודא שהוא מחובר לאותה רשת אליה אתה מחובר. + נראה שאתה מחובר דרך הרשת הסלולרית. KDE Connect עובד רק עם רשתות מקומיות. לא נמצאו מנהלי קבצים מותקנים במכשיר זה. - שלח SMS + שליחת הודעת SMS שלח הודעות מהמחשב שלך - התוסף הזה לא נתמך ע\"י המכשיר שלך - מצא את המכשיר שלי + תוסף הזה לא נתמך על ידי המכשיר שלך + מצא את הפלאפון שלי + מצא את הטבלט שלי + מפעיל רעש במכשיר כדי שתוכל למצוא אותו. נמצא פתח סגור diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index c3298781..fbc2f613 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -47,9 +47,6 @@ Clic centrale Niente - destra - centro - predefinita Minima Più veloce diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index 55b661aa..2a89b9ea 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -41,9 +41,6 @@ 가운데 단추 누름 아무것도 안 함 - 오른쪽 - 가운데 - 기본값 느리게 느리게 이상 diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml index 307b9292..603c07a0 100644 --- a/res/values-lt/strings.xml +++ b/res/values-lt/strings.xml @@ -41,14 +41,11 @@ Jau paprašyta suporuoti Įrenginys jau suporuotas Nepavyksta išsiųsti paketo + Skirtasis laikas baigėsi Naudotojas atšaukė užduotį Porininkas atšaukė užduotį Gautas netinkamas raktas Paprašyta suporuoti - %1s - %1s - %1s - %1s Norėdami atsakyti, palieskite Prisijungti iš naujo Rodyti klaviatūrą @@ -86,7 +83,6 @@ Suporuoti naują įrenginį Atrišti %s Pridėti įrenginį pagal IP - Vidinė saugykla Visi failai SD kortelė %d SD kortelė @@ -100,9 +96,12 @@ Čia turėtų pasirodyti to kiti paties tinklo įrenginiai, kuriuose veikia „KDE Connect“ Įrenginys suporuotas Pervadinti įrenginį + Pervadinti Atnaujinti Šis suporuotas įrenginys nepasiekiamas. Patikrinkite, ar jis prisijungęs prie to paties tinklo. Siųsti SMS Telefonas nepalaiko šio papildinio Radau + Atverti + Užverti diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index ef92ae6c..61f1fce6 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -47,9 +47,6 @@ Middelste muisklik Niets - rechts - midden - standaard Langzaamst Langzaam @@ -78,22 +75,22 @@ Het andere apparaat gebruikt geen recente versie van KDE Connect, de verouderde versleutelingsmethode zal worden gebruikt. De SHA1 vingerafdruk van het certificaat van uw apparaat is: De SHA1 vingerafdruk van het certificaat van het apparaat op afstand is: - Paar gevraagd + Paarvorming gevraagd Verzoek om een paar te maken van %1s Ontvangen koppeling van %1s Tap om \'%1s\' te openen Inkomend bestand van %1s %1s Bezig bestand te verzenden naar %1s - Bestanden verzenden naar %1s + Bezig bestanden te verzenden naar %1s %1s - Verzonden %1$d van %2$d bestanden + Verzonden %1$d uit %2$d bestanden Bestand ontvangen van %1s Bestand ontvangen van %1s is mislukt Tap om \'%1s\' te openen Bestand verzonden naar %1s %1s - Verzenden van bestanden naar %1s is mislukt + Verzenden van bestand naar %1s is mislukt %1s Tap om te antwoorden Opnieuw verbinden @@ -140,7 +137,7 @@ Voeg apparaten toe per IP-adres Luidruchtige meldingen Vibreer en speel een geluidje bij ontvangen van een bestand - Pas bestemmingsmap aan + De bestemmingsmap aanpassen Ontvangen bestanden zullen in Downloads verschijnen Bestanden zullen opgeslagen worden in de onderstaande map Bestemmingsmap @@ -176,4 +173,7 @@ Gevonden Openen Sluiten + U moet toestemming geven voor toegang tot de opslag + Sommige plug-ins hebben toestemming nodig om te werken (tik voor meer informatie): + Deze plug-in heeft toestemming nodig om te werken diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 5e6630c0..84682a04 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -47,9 +47,6 @@ Kliknięcie środkowym Nic - prawo - środek - domyślne Najmniejsza Ponad najmniejszą @@ -176,4 +173,7 @@ Znaleziony Otwórz Zamknij + Musisz nadać uprawnienia, aby uzyskać dostęp do pamięci masowej + Niektóre z wtyczek wymagają uprawnień do działania (stuknij po więcej informacji) + Ta wtyczka wymaga uprawnień do działania diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index c688b029..816dc563 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -39,9 +39,6 @@ Botão do meio Nada - direita - meio - padrão Mais lento Ainda mais lento diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml index 193d21a1..9c053dee 100644 --- a/res/values-pt/strings.xml +++ b/res/values-pt/strings.xml @@ -47,9 +47,6 @@ Botão do meio Nada - direita - meio - predefinição Mais Lento Ainda Mais Lento @@ -176,4 +173,7 @@ Encontrado Abrir Fechar + Precisa de dar permissões de acesso ao armazenamento + Alguns \'plugins\' precisam de permissões para funcionar (toque para mais informações): + Este \'plugin\' precisa de permissões para funcionar diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml index b158da24..54222404 100644 --- a/res/values-sk/strings.xml +++ b/res/values-sk/strings.xml @@ -10,8 +10,6 @@ Zdieľať obsah schránky Vzdialený vstup Použiť váš telefón alebo tablet ako touchpad a klávesnicu - Prijímať vzdialené stlačenia klávesov - Prijímať udalosti stlačení klávesov od vzdialených zariadení Multimediálne ovládače Poskytuje vzdialené ovládanie pre váš prehrávač médií Spustiť príkaz @@ -32,10 +30,6 @@ Musíte povoliť oprávnenia na prístup k pripomienkam Poslať ping Multimediálny ovládač - Spracúva vzdialené klávesy len pri editácii - Nie sú žiadne aktívne pripojenia vzdialenej klávesnice, vytvorte nejaké v Kdeconnect - Vzdialené pripojenie klávesnice je aktívne - Je viac ako jedno vzdialené pripojenie klávesnice, vyberte zariadenie na nastavenie Vzdialený vstup Posúvajte prst na obrazovke na posun kurzora. Ťuknutie vyvolá klik a použite dva/tri prsty pre pravé a stredné tlačidlo. Použite dlhé stlačenie pre drag and drop. Nastaviť akciu dvoch prstov @@ -47,9 +41,6 @@ Stredný klik Nič - vpravo - stred - predvolené Najpomalšie Nad najpomalším @@ -85,15 +76,12 @@ Prichádzajúci súbor od %s %1s Posielam súbor pre %1s - Posielam súbor pre %1s %1s - Poslať %1$d z %2$d súborov Prijatý súbor od %1s Zlyhalo prijatie súboru od %1s Ťuknite na otvorenie \'%1s\' Poslať súbor pre %1s %1s - Zlyhalo poslanie súboru %1s %1s Tapnite na odpoveď Znovu pripojiť @@ -140,10 +128,6 @@ Pridať zariadenia podľa IP Hlučné pripomienky Vibrovať a prehrať zvuk pri prijatí súboru - Prispôsobiť cieľový adresár - Prijaté súbory sa objavia v Preberaniach - Súbory sa uložia v adresári dolu - Cieľový adresár Filter upozornení Upozornenia budú synchronizované pre vybrané aplikácie. Interné úložisko @@ -165,7 +149,6 @@ Premenovať Obnoviť Toto spárované zariadenie nie je dosiahnuteľné. Prosím, uistite sa, že je pripojené do rovnakej siete. - Zdá sa, že ste na mobilom dátovom pripojení. KDE Connect funguje iba na lokálnej sieti. Nie sú nainštalované žiadne prehliadače. Poslať SMS Posielať textové správy z vášho počítača diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index 423fac73..c05ba613 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -176,4 +176,7 @@ Нађен Отвори Затвори + Морате дати дозволе за приступ унутрашњој меморији + Неки прикључци траже дозволе да би радили (тапните за више информација): + Овај прикључак тражи дозволе да би радио diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index 4e614b96..8ceaee0b 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -8,10 +8,10 @@ Gör det möjligt att bläddra i apparatens filsystem från annan apparat Synkronisera klippbord Dela klippbordets innehåll - Fjärrinmatning + Extern inmatning Använd telefonen eller surfplattan som mus och tangentbord - Ta emot fjärrtangentnedtryckningar - Ta emot tangentnedtryckningar från fjärrenheter + Ta emot externa tangentnedtryckningar + Ta emot tangentnedtryckningar från externa enheter Multimediakontroller Tillhandahåller en fjärrkontroll för mediaspelaren Kör kommando @@ -32,11 +32,11 @@ Du måste ge rättighet att komma åt underrättelser Skicka ping Kontroll av multimedia - Hantera bara fjärrtangenter vid redigering - Det finns ingen aktiv fjärrtangentbordsanslutning, upprätta en i KDE-anslut - Fjärrtangentbordsanslutning är aktiv - Det finns mer än en fjärrtangentbordsanslutning, välj enhet att anpassa - Fjärrinmatning + Hantera bara externa tangenter vid redigering + Det finns ingen aktiv anslutning till externt tangentbord, upprätta en i KDE-anslut + Anslutning till externt tangentbord är aktiv + Det finns mer än en anslutning till externt tangentbord, välj enhet att anpassa + Extern inmatning Flytta fingret på skärmen för att röra muspekaren. Rör för att klicka, och använd två eller tre fingrar för höger- och mittenknapparna. Använd en längre beröring för drag och släpp. Ställ in åtgärd vid två fingerberöringar Ställ in åtgärd vid tre fingerberöringar @@ -47,9 +47,6 @@ Mittenklick Ingenting - höger - mitten - normal Långsammaste Ovanför långsammaste @@ -176,4 +173,7 @@ Hittade den Öppna Stäng + Du måste ge rättighet att komma åt lagringen + Vissa insticksprogram kräver rättigheter för att fungera (rör för mer information): + Insticksprogrammet behöver rättigheter för att fungera diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml new file mode 100644 index 00000000..ab4776d8 --- /dev/null +++ b/res/values-tr/strings.xml @@ -0,0 +1,179 @@ + + + Telefon bildiricisi + SMS ve çağrılar için bildirimler yolla + Batarya raporu + Batarya durumunu belirli aralıklarla raporla + Dosya sistemi gösterme + Bu aıygıtın dosya sistemine uzaktan gözatılmasına izin verir + Pano eşitleme + Pano içeriğini paylaş + Uzak girdi + Telefonunuzu veya tabletinizi, dokunmatik veya klavye olarak kullanın + Uzak tuşa basmaları getir + Tuş basma eylemlerini, uzak aygıtlardan getir + Çoklu ortam denetimleri + Ortam oynatıcınız için uzak denetim sağlar + Komut Çalıştır + Uzak komutları, telefon veya tabletinizden tetikler + Ping + Ping gönder ve al + Bildirim eşitleme + Bildirimlerinize, diğer aygıtlardan erişin + Bildirimleri al + Bildirimleri diğer aygıtlardan al ve Android üzerinde göster + Paylaş ve al + Dosyaları ve URL\'leri aygıtlar arasında paylaş + Bu özellik, sahip olduğunuz Android sürümünde kullanılabilir değil + Aygıt yok + Tamam + İptal + Ayarları aç + Bildirimler erişebilmek için izine ihtiyacınız var + Ping gönder + Çoklu ortam denetimi + Uzak tuşları, sadece düzenleme yaparken işle + Etkin bir uzak klavye bağlantısı yok, kdeconnect ile bir bağlantı kurun + Uzak klavye bağlantısı etkin + Birden çok uzak klavye bağlantısı mevcut, yapılandırmak istediğiniz aygıtı seçin + Girdi sil + Fare imlecini hareket ettirmek için ekran üzerinde parmaklarınızı hareket ettirin. Tek tıklama için dokunun, sağ ve orta düğmeler için iki/üç parmak kullanın. Sürüklemek için uzun basın. + İki parmak dokunma eylemini ayarla + Üç parmak dokunma eylemini ayarla + Dokunmatik yüzey hassasiyetini ayarla + Ters Kaydırma Yönü + + Sağ tık + Orta tık + Hiçbiri + + + En Yavaş + En Yavaşın Üstü + Varsayıla + Varsayılan Üstü + En Hızlı + + Bağlı aygıtlar + Kullanılabilir aygıtlar + Hatırlanan aygıtlar + Eklentiler yüklenemedi (daha fazla bilgi için dokunun): + Eklenti ayarları + Ayır + Eşleşmiş aygıt ulaşılabilir değil + Yeni bir aygıt eşleştir + Bİlinmeyen aygıt + Aygıt ulaşılabilir değil + Eşleşme zaten talep edilmiş + Aygıt zaten eşleşmiş + Paket gönderilemedi + Zaman aşımı + Kullanıcı tarafından iptal edildi + Diğer eş tarafından iptal edildi + Geçersiz anahtar alındı + Şifreleme Bilgisi + Diğer aygıt, KDE Connect\'in son sürümünü kullanmıyor, eski şifreleme yöntemini kullanıyor. + Aygıt sertifikanızın SHA1 parmak izi: + Uzak aygıt sertifikanızın SHA1 parmak izi: + Eşleşme talep edildi + %1s için eşleşme talebi + %1s üzerinden bağlantı alındı + \'%1s\' açmak için dokunun + %1s üzerinden gelen dosya + %1s + Dosya şuraya gönderiliyor, %1s + Dosyalar şuraya gönderiliyor, %1s + %1s + %2$d dosyadan %1$d dosya gönderildi + Şuradan dosya alındı, %1s + Şuradan dosya alma başarısız, %1s + \'%1s\' açmak için dokunun + Dosyayı şuraya gönder, %1s + %1s + Dosyayı şuraya gönderme başarısız, %1s + %1s + Cevap için dokunun + Yeniden Bağlan + Sağ Tık Gönder + Orta Tık Gönder + Klavyeyi Göster + Aygıt eşleşmemiş + Eşleşme isteği + Onayla + Reddet + Aygıt + Aygıt eşleştir + Uzak denetim + KDE Connect Ayarları + Oynat + Önceki + Geri Sar + Hızlı İleri Sar + Sonraki + Ses + Çoklu Ortam Ayarları + İleri/geri düğmeleri + Basıldığında kullanılacak ileri/geri zamanını ayarlayın. + + 10 saniye + 20 saniye + 30 saniye + 1 dakika + 2 dakika + + Paylaş... + Bu aygıt, eski bir protokol sürümü kullanıyor + Bu aygıt, daha yeni bir protokol sürümü kullanıyor + Genel Ayarlar + Ayarlar + %s ayarları + Aygıt adı + %s + Geçersiz aygıt adı + Gelen ileti, panoya kaydet + Özel aygıt listesi + Yeni bir aygıt eşleştir + Ayır %s + IP\'ye göre aygıtları ekle + Sesli bildirimler + Bir dosya alırken, ses çıkar ve titret + Hedef dizini özelleştir + Gelen dosyalar İndirilenler\'de gözükecektir + Dosyalar aşağıdaki dizinden depolanacaktır + Hedef dizin + Bildirim süzgeci + Bildirimler, seçili uygulamalar için eşitlenecektir. + Harici depolama + Tüm dosyalar + SD kart %d + SD kart + (salt okunur) + Kamera resimleri + Makine/IP ekle + Makine adı veya IP + Onatıcı bulunamadı + Bu seçeneği, sadece aygıtınız otomatik bulunamadıysa kullanın. Aşağıya IP adresini veya makine adının girin ve listeye eklemek için düğmeye dokunun. Listeden bir ögeyi silmek için, mevcut ögeye tıklayın. + %2$s üzerindeki %1$s + Dosyaları gönder + KDE Connect Aygıtları + KDE Connect\'te çalışan, aynı ağdaki diğer aygıtlar burada gözükmelidir. + Aygıt eşleştirildi + Aygıtı yeniden adlandır + Yeniden adlandır + Tazele + Eşleştirilmiş aygıt ulaşılabilir değil. Aynı ağa bağlı olduğundan emin olun. + Mobil veri bağlantısında olduğunuz gözüküyor. KDE Connect sadece yerel ağlarda çalışır. + Yüklü bir dosya tarayıcısı yok. + SMS Gönder + Masaüstünden metin iletisi gönder + Eklenti, aygıt tarafından desteklenmiyor + Telefonumu bul + Tabletimi bul + Aygıtı bulmak için onu çaldır + Bulundu + + Kapat + Depolamaya erişim için izne ihtiyacınız var + Bazı Eklentiler çalışmak için izne ihtiyaç duyar (daha fazla bilgi için dokunun): + Bu eklenti, çalışmak için izne ihtiyaç duyuyor + diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index 70e55b1e..99648101 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -47,9 +47,6 @@ Клацання середньою Нічого - права - середня - типва Найповільніший Швидший за найповільніший @@ -176,4 +173,7 @@ Знайдено Відкрити Закрити + Для доступу до сховища даних вам слід надати програмі права доступу + Для роботи деяких додатків потрібні додаткові права доступу (натисніть, щоб дізнатися більше): + Для роботи цього додатка потрібні додаткові права доступу diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index 894e7bdc..78bd446e 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -47,9 +47,6 @@ 中键点击 - - - 默认 最慢 高于最慢 @@ -170,6 +167,9 @@ 从桌面发送短消息 设备不支持此插件 找到我的手机 + 找到我的平板电脑 让设备响铃从而找到它 找到 + 打开 + 关闭 diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml index abcc34a6..715ae130 100644 --- a/res/values-zh-rTW/strings.xml +++ b/res/values-zh-rTW/strings.xml @@ -41,9 +41,6 @@ 中鍵點擊 - - - 預設 最慢 diff --git a/res/values/strings.xml b/res/values/strings.xml index f78801be..8934a81d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -53,9 +53,9 @@ Middle click Nothing - right - middle - default + right + middle + default right middle @@ -206,4 +206,8 @@ Open Close + You need to grant permissions to access the storage + Some Plugins need permissions to work (tap for more info): + This plugin needs permissions to work + diff --git a/res/xml/mousepadplugin_preferences.xml b/res/xml/mousepadplugin_preferences.xml index f5b8f5d6..dbafa886 100644 --- a/res/xml/mousepadplugin_preferences.xml +++ b/res/xml/mousepadplugin_preferences.xml @@ -5,30 +5,30 @@ + android:key="@string/mousepad_double_tap_key" + android:summary="%s" + android:title="@string/mousepad_double_tap_settings_title"/> + android:key="@string/mousepad_triple_tap_key" + android:summary="%s" + android:title="@string/mousepad_triple_tap_settings_title"/> + android:key="@string/mousepad_sensitivity_key" + android:summary="%s" + android:title="@string/mousepad_sensitivity_settings_title"/> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package org.kde.kdeconnect.Backends.BluetoothBackend; + +import android.annotation.TargetApi; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothServerSocket; +import android.bluetooth.BluetoothSocket; +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; +import org.kde.kdeconnect.Backends.BaseLink; +import org.kde.kdeconnect.Backends.BasePairingHandler; +import org.kde.kdeconnect.Device; +import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper; +import org.kde.kdeconnect.NetworkPackage; + +import java.io.*; +import java.nio.charset.Charset; +import java.security.PublicKey; +import java.util.UUID; + +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +public class BluetoothLink extends BaseLink { + private final BluetoothSocket socket; + private final BluetoothLinkProvider linkProvider; + + private boolean continueAccepting = true; + + private Thread receivingThread = new Thread(new Runnable() { + @Override + public void run() { + StringBuilder sb = new StringBuilder(); + try { + Reader reader = new InputStreamReader(socket.getInputStream(), "UTF-8"); + char[] buf = new char[512]; + while (continueAccepting) { + while (sb.indexOf("\n") == -1 && continueAccepting) { + int charsRead; + if ((charsRead = reader.read(buf)) > 0) { + sb.append(buf, 0, charsRead); + } + } + + int endIndex = sb.indexOf("\n"); + if (endIndex != -1) { + String message = sb.substring(0, endIndex + 1); + sb.delete(0, endIndex + 1); + processMessage(message); + } + } + } catch (IOException e) { + Log.e("BluetoothLink/receiving", "Connection to " + socket.getRemoteDevice().getAddress() + " likely broken.", e); + disconnect(); + } + } + + private void processMessage(String message) { + NetworkPackage np; + try { + np = NetworkPackage.unserialize(message); + } catch (JSONException e) { + Log.e("BluetoothLink/receiving", "Unable to parse message.", e); + return; + } + + if (np.getType().equals(NetworkPackage.PACKAGE_TYPE_ENCRYPTED)) { + try { + np = RsaHelper.decrypt(np, privateKey); + } catch(Exception e) { + Log.e("BluetoothLink/receiving", "Exception decrypting the package", e); + } + } + + if (np.hasPayloadTransferInfo()) { + BluetoothSocket transferSocket = null; + try { + UUID transferUuid = UUID.fromString(np.getPayloadTransferInfo().getString("uuid")); + transferSocket = socket.getRemoteDevice().createRfcommSocketToServiceRecord(transferUuid); + transferSocket.connect(); + np.setPayload(transferSocket.getInputStream(), np.getPayloadSize()); + } catch (Exception e) { + if (transferSocket != null) { + try { transferSocket.close(); } catch(IOException ignored) { } + } + Log.e("BluetoothLink/receiving", "Unable to get payload", e); + } + } + + packageReceived(np); + } + }); + + public BluetoothLink(Context context, BluetoothSocket socket, String deviceId, BluetoothLinkProvider linkProvider) { + super(context, deviceId, linkProvider); + this.socket = socket; + this.linkProvider = linkProvider; + } + + public void startListening() { + this.receivingThread.start(); + } + + @Override + public String getName() { + return "BluetoothLink"; + } + + @Override + public BasePairingHandler getPairingHandler(Device device, BasePairingHandler.PairingHandlerCallback callback) { + return new BluetoothPairingHandler(device, callback); + } + + public void disconnect() { + if (socket == null) { + return; + } + continueAccepting = false; + try { + socket.close(); + } catch (IOException e) { + } + linkProvider.disconnectedLink(this, getDeviceId(), socket); + } + + private void sendMessage(NetworkPackage np) throws JSONException, IOException { + byte[] message = np.serialize().getBytes(Charset.forName("UTF-8")); + OutputStream socket = this.socket.getOutputStream(); + Log.i("BluetoothLink","Beginning to send message"); + socket.write(message); + Log.i("BluetoothLink","Finished sending message"); + } + + @Override + public boolean sendPackage(NetworkPackage np, Device.SendPackageStatusCallback callback) { + return sendPackageInternal(np, callback, null); + } + + @Override + public boolean sendPackageEncrypted(NetworkPackage np, Device.SendPackageStatusCallback callback, PublicKey key) { + return sendPackageInternal(np, callback, key); + } + + private boolean sendPackageInternal(NetworkPackage np, final Device.SendPackageStatusCallback callback, PublicKey key) { + + /*if (!isConnected()) { + Log.e("BluetoothLink", "sendPackageEncrypted failed: not connected"); + callback.sendFailure(new Exception("Not connected")); + return; + }*/ + + try { + BluetoothServerSocket serverSocket = null; + if (np.hasPayload()) { + UUID transferUuid = UUID.randomUUID(); + serverSocket = BluetoothAdapter.getDefaultAdapter() + .listenUsingRfcommWithServiceRecord("KDE Connect Transfer", transferUuid); + JSONObject payloadTransferInfo = new JSONObject(); + payloadTransferInfo.put("uuid", transferUuid.toString()); + np.setPayloadTransferInfo(payloadTransferInfo); + } + + if (key != null) { + try { + np = RsaHelper.encrypt(np, key); + } catch (Exception e) { + callback.onFailure(e); + return false; + } + } + + sendMessage(np); + + if (serverSocket != null) { + BluetoothSocket transferSocket = serverSocket.accept(); + try { + serverSocket.close(); + + int idealBufferLength = 4096; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && transferSocket.getMaxReceivePacketSize() > 0) { + idealBufferLength = transferSocket.getMaxReceivePacketSize(); + } + byte[] buffer = new byte[idealBufferLength]; + int bytesRead; + long progress = 0; + InputStream stream = np.getPayload(); + while ((bytesRead = stream.read(buffer)) != -1) { + progress += bytesRead; + transferSocket.getOutputStream().write(buffer, 0, bytesRead); + if (np.getPayloadSize() > 0) { + callback.onProgressChanged((int) (100 * progress / np.getPayloadSize())); + } + } + transferSocket.getOutputStream().flush(); + stream.close(); + } catch (Exception e) { + callback.onFailure(e); + return false; + } finally { + try { transferSocket.close(); } catch (IOException ignored) { } + } + } + + callback.onSuccess(); + return true; + } catch (Exception e) { + callback.onFailure(e); + return false; + } + } + + @Override + public boolean linkShouldBeKeptAlive() { + return receivingThread.isAlive(); + } + + /* + public boolean isConnected() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { + return socket.isConnected(); + } else { + return true; + } + } +*/ +} diff --git a/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLinkProvider.java b/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLinkProvider.java new file mode 100644 index 00000000..629b1c6e --- /dev/null +++ b/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothLinkProvider.java @@ -0,0 +1,378 @@ +/* + * Copyright 2016 Saikrishna Arcot + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package org.kde.kdeconnect.Backends.BluetoothBackend; + +import android.annotation.TargetApi; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; +import android.bluetooth.BluetoothServerSocket; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.Parcelable; +import android.util.Log; + +import org.kde.kdeconnect.Backends.BaseLinkProvider; +import org.kde.kdeconnect.Device; +import org.kde.kdeconnect.NetworkPackage; + +import java.io.*; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.Set; + +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +public class BluetoothLinkProvider extends BaseLinkProvider { + + private static final UUID SERVICE_UUID = UUID.fromString("185f3df4-3268-4e3f-9fca-d4d5059915bd"); + private static final int REQUEST_ENABLE_BT = 48; + + private final Context context; + private final Map visibleComputers = new HashMap<>(); + private final Map sockets = new HashMap<>(); + + private BluetoothAdapter bluetoothAdapter = null; + + private ServerRunnable serverRunnable; + private ClientRunnable clientRunnable; + + private void addLink(NetworkPackage identityPackage, BluetoothLink link) { + String deviceId = identityPackage.getString("deviceId"); + Log.i("BluetoothLinkProvider","addLink to "+deviceId); + BluetoothLink oldLink = visibleComputers.get(deviceId); + if (oldLink == link) { + Log.e("BluetoothLinkProvider", "oldLink == link. This should not happen!"); + return; + } + visibleComputers.put(deviceId, link); + connectionAccepted(identityPackage, link); + link.startListening(); + if (oldLink != null) { + Log.i("BluetoothLinkProvider","Removing old connection to same device"); + oldLink.disconnect(); + } + } + + public BluetoothLinkProvider(Context context) { + this.context = context; + + bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (bluetoothAdapter == null) { + Log.e("BluetoothLinkProvider","No bluetooth adapter found."); + } + } + + @Override + public void onStart() { + if (bluetoothAdapter == null) { + return; + } + + if (!bluetoothAdapter.isEnabled()) { + Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + Log.e("BluetoothLinkProvider","Bluetooth adapter not enabled."); + // TODO: next line needs to be called from an existing activity, so move it? + // startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); + // TODO: Check result of the previous command, whether the user allowed bluetooth or not. + return; + } + + //This handles the case when I'm the existing device in the network and receive a hello package + clientRunnable = new ClientRunnable(); + new Thread(clientRunnable).start(); + + // I'm on a new network, let's be polite and introduce myself + serverRunnable = new ServerRunnable(); + new Thread(serverRunnable).start(); + } + + @Override + public void onNetworkChange() { + onStop(); + onStart(); + } + + @Override + public void onStop() { + if (bluetoothAdapter == null || clientRunnable == null || serverRunnable == null) { + return; + } + + clientRunnable.stopProcessing(); + serverRunnable.stopProcessing(); + } + + @Override + public String getName() { + return "BluetoothLinkProvider"; + } + + public void disconnectedLink(BluetoothLink link, String deviceId, BluetoothSocket socket) { + sockets.remove(socket.getRemoteDevice()); + visibleComputers.remove(deviceId); + connectionLost(link); + } + + private class ServerRunnable implements Runnable { + + private boolean continueProcessing = true; + private BluetoothServerSocket serverSocket; + + void stopProcessing() { + continueProcessing = false; + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public void run() { + try { + serverSocket = bluetoothAdapter + .listenUsingRfcommWithServiceRecord("KDE Connect", SERVICE_UUID); + } catch (IOException e) { + e.printStackTrace(); + return; + } + + if (continueProcessing) { + try { + BluetoothSocket socket = serverSocket.accept(); + connect(socket); + } catch (Exception ignored) { + } + } + } + + private void connect(BluetoothSocket socket) throws Exception { + //socket.connect(); + OutputStream outputStream = socket.getOutputStream(); + if (sockets.containsKey(socket.getRemoteDevice())) { + Log.i("BTLinkProvider/Server", "Received duplicate connection from " + socket.getRemoteDevice().getAddress()); + socket.close(); + return; + } else { + sockets.put(socket.getRemoteDevice(), socket); + } + + Log.i("BTLinkProvider/Server", "Received connection from " + socket.getRemoteDevice().getAddress()); + + NetworkPackage np = NetworkPackage.createIdentityPackage(context); + byte[] message = np.serialize().getBytes("UTF-8"); + outputStream.write(message); + + Log.i("BTLinkProvider/Server", "Sent identity package"); + + // Listen for the response + StringBuilder sb = new StringBuilder(); + Reader reader = new InputStreamReader(socket.getInputStream(), "UTF-8"); + int charsRead; + char[] buf = new char[512]; + while(sb.lastIndexOf("\n") == -1 && (charsRead = reader.read(buf)) != -1) { + sb.append(buf, 0, charsRead); + } + + String response = sb.toString(); + final NetworkPackage identityPackage = NetworkPackage.unserialize(response); + + if (!identityPackage.getType().equals(NetworkPackage.PACKAGE_TYPE_IDENTITY)) { + Log.e("BTLinkProvider/Server", "2 Expecting an identity package"); + return; + } + + Log.i("BTLinkProvider/Server", "Received identity package"); + + BluetoothLink link = new BluetoothLink(context, socket, + identityPackage.getString("deviceId"), BluetoothLinkProvider.this); + + addLink(identityPackage, link); + } + } + + private class ClientRunnable extends BroadcastReceiver implements Runnable { + + private boolean continueProcessing = true; + private Map connectionThreads = new HashMap<>(); + + void stopProcessing() { + continueProcessing = false; + } + + @Override + public void run() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_UUID); + context.registerReceiver(this, filter); + } + + while (continueProcessing) { + connectToDevices(); + try { + Thread.sleep(15000); + } catch (InterruptedException ignored) { + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + context.unregisterReceiver(this); + } + } + + private void connectToDevices() { + Set pairedDevices = bluetoothAdapter.getBondedDevices(); + Log.i("BluetoothLinkProvider", "Bluetooth adapter paired devices: " + pairedDevices.size()); + + // Loop through paired devices + for (BluetoothDevice device : pairedDevices) { + if (sockets.containsKey(device)) { + continue; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + device.fetchUuidsWithSdp(); + } else { + connectToDevice(device); + } + } + } + + @Override + @TargetApi(value=Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(BluetoothDevice.ACTION_UUID)) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + Parcelable[] activeUuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID); + + if (sockets.containsKey(device)) { + return; + } + + if (activeUuids == null) { + return; + } + + for (Parcelable uuid: activeUuids) { + if (uuid.toString().equals(SERVICE_UUID.toString())) { + connectToDevice(device); + return; + } + } + } + } + + private void connectToDevice(BluetoothDevice device) { + if (!connectionThreads.containsKey(device) || !connectionThreads.get(device).isAlive()) { + Thread connectionThread = new Thread(new ClientConnect(device)); + connectionThread.start(); + connectionThreads.put(device, connectionThread); + } + } + + + } + + private class ClientConnect implements Runnable { + + private final BluetoothDevice device; + + public ClientConnect(BluetoothDevice device) { + this.device = device; + } + + @Override + public void run() { + connectToDevice(); + } + + private void connectToDevice() { + BluetoothSocket socket; + try { + socket = device.createRfcommSocketToServiceRecord(SERVICE_UUID); + socket.connect(); + sockets.put(device, socket); + } catch (IOException e) { + Log.e("BTLinkProvider/Client", "Could not connect to KDE Connect service on " + device.getAddress(), e); + return; + } + + Log.i("BTLinkProvider/Client", "Connected to " + device.getAddress()); + + try { + int character; + StringBuilder sb = new StringBuilder(); + while(sb.lastIndexOf("\n") == -1 && (character = socket.getInputStream().read()) != -1) { + sb.append((char)character); + } + + String message = sb.toString(); + final NetworkPackage identityPackage = NetworkPackage.unserialize(message); + + if (!identityPackage.getType().equals(NetworkPackage.PACKAGE_TYPE_IDENTITY)) { + Log.e("BTLinkProvider/Client", "1 Expecting an identity package"); + socket.close(); + return; + } + + Log.i("BTLinkProvider/Client", "Received identity package"); + + String myId = NetworkPackage.createIdentityPackage(context).getString("deviceId"); + if (identityPackage.getString("deviceId").equals(myId)) { + // Probably won't happen, but just to be safe + socket.close(); + return; + } + + if (visibleComputers.containsKey(identityPackage.getString("deviceId"))) { + return; + } + + Log.i("BTLinkProvider/Client", "Identity package received, creating link"); + + final BluetoothLink link = new BluetoothLink(context, socket, + identityPackage.getString("deviceId"), BluetoothLinkProvider.this); + + NetworkPackage np2 = NetworkPackage.createIdentityPackage(context); + link.sendPackage(np2,new Device.SendPackageStatusCallback() { + @Override + public void onSuccess() { + addLink(identityPackage, link); + } + + @Override + public void onFailure(Throwable e) { + + } + }); + } catch (Exception e) { + Log.e("BTLinkProvider/Client", "Connection lost/disconnected on " + device.getAddress(), e); + } + } + } +} diff --git a/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothPairingHandler.java b/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothPairingHandler.java new file mode 100644 index 00000000..1d380e05 --- /dev/null +++ b/src/org/kde/kdeconnect/Backends/BluetoothBackend/BluetoothPairingHandler.java @@ -0,0 +1,193 @@ +/* + * Copyright 2015 Vineet Garg + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package org.kde.kdeconnect.Backends.BluetoothBackend; + +import android.util.Log; +import org.kde.kdeconnect.Backends.BasePairingHandler; +import org.kde.kdeconnect.Device; +import org.kde.kdeconnect.NetworkPackage; +import org.kde.kdeconnect_tp.R; + +import java.util.Timer; +import java.util.TimerTask; + +public class BluetoothPairingHandler extends BasePairingHandler { + + Timer mPairingTimer; + public BluetoothPairingHandler(Device device, final PairingHandlerCallback callback) { + super(device, callback); + + if (device.isPaired()) { + mPairStatus = PairStatus.Paired; + } else { + mPairStatus = PairStatus.NotPaired; + } + } + +// @Override + public NetworkPackage createPairPackage() { + NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PAIR); + np.set("pair", true); + return np; + } + + @Override + public void packageReceived(NetworkPackage np) throws Exception{ + + boolean wantsPair = np.getBoolean("pair"); + + if (wantsPair == isPaired()) { + if (mPairStatus == PairStatus.Requested) { + //Log.e("Device","Unpairing (pair rejected)"); + mPairStatus = PairStatus.NotPaired; + hidePairingNotification(); + mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_canceled_by_other_peer)); + } + return; + } + + if (wantsPair) { + + if (mPairStatus == PairStatus.Requested) { //We started pairing + hidePairingNotification(); + pairingDone(); + } else { + + // If device is already paired, accept pairing silently + if (mDevice.isPaired()) { + acceptPairing(); + return; + } + + // Pairing notifications are still managed by device as there is no other way to + // know about notificationId to cancel notification when PairActivity is started + // Even putting notificationId in intent does not work because PairActivity can be + // started from MainActivity too, so then notificationId cannot be set + hidePairingNotification(); + mDevice.displayPairingNotification(); + + mPairingTimer = new Timer(); + + mPairingTimer.schedule(new TimerTask() { + @Override + public void run() { + Log.w("KDE/Device","Unpairing (timeout B)"); + mPairStatus = PairStatus.NotPaired; + hidePairingNotification(); + } + }, 25*1000); //Time to show notification, waiting for user to accept (peer will timeout in 30 seconds) + mPairStatus = PairStatus.RequestedByPeer; + mCallback.incomingRequest(); + + } + } else { + Log.i("KDE/Pairing", "Unpair request"); + + if (mPairStatus == PairStatus.Requested) { + hidePairingNotification(); + mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_canceled_by_other_peer)); + } else if (mPairStatus == PairStatus.Paired) { + mCallback.unpaired(); + } + + mPairStatus = PairStatus.NotPaired; + + } + + } + + @Override + public void requestPairing() { + + Device.SendPackageStatusCallback statusCallback = new Device.SendPackageStatusCallback() { + @Override + public void onSuccess() { + hidePairingNotification(); //Will stop the pairingTimer if it was running + mPairingTimer = new Timer(); + mPairingTimer.schedule(new TimerTask() { + @Override + public void run() { + mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_timed_out)); + Log.w("KDE/Device","Unpairing (timeout A)"); + mPairStatus = PairStatus.NotPaired; + } + }, 30*1000); //Time to wait for the other to accept + mPairStatus = PairStatus.Requested; + } + + @Override + public void onFailure(Throwable e) { + mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_could_not_send_package)); + } + }; + mDevice.sendPackage(createPairPackage(), statusCallback); + } + + public void hidePairingNotification() { + mDevice.hidePairingNotification(); + if (mPairingTimer != null) { + mPairingTimer .cancel(); + } + } + + @Override + public void acceptPairing() { + hidePairingNotification(); + Device.SendPackageStatusCallback statusCallback = new Device.SendPackageStatusCallback() { + @Override + public void onSuccess() { + pairingDone(); + } + + @Override + public void onFailure(Throwable e) { + mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_not_reachable)); + } + }; + mDevice.sendPackage(createPairPackage(), statusCallback); + } + + @Override + public void rejectPairing() { + hidePairingNotification(); + mPairStatus = PairStatus.NotPaired; + NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PAIR); + np.set("pair", false); + mDevice.sendPackage(np); + } + + //@Override + public void pairingDone() { + // Store device information needed to create a Device object in a future + //Log.e("KDE/PairingDone", "Pairing Done"); + mPairStatus = PairStatus.Paired; + mCallback.pairingDone(); + + } + + @Override + public void unpair() { + mPairStatus = PairStatus.NotPaired; + NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PAIR); + np.set("pair", false); + mDevice.sendPackage(np); + } +} diff --git a/src/org/kde/kdeconnect/BackgroundService.java b/src/org/kde/kdeconnect/BackgroundService.java index ca6c2cca..16c5efc1 100644 --- a/src/org/kde/kdeconnect/BackgroundService.java +++ b/src/org/kde/kdeconnect/BackgroundService.java @@ -26,11 +26,13 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Binder; +import android.os.Build; import android.os.IBinder; import android.util.Log; import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BaseLinkProvider; +//import org.kde.kdeconnect.Backends.BluetoothBackend.BluetoothLinkProvider; import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider; import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; @@ -140,11 +142,11 @@ public class BackgroundService extends Service { } private void registerLinkProviders() { - - //linkProviders.add(new LoopbackLinkProvider(this)); - linkProviders.add(new LanLinkProvider(this)); - +// linkProviders.add(new LoopbackLinkProvider(this)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { +// linkProviders.add(new BluetoothLinkProvider(this)); + } } public ArrayList getLinkProviders() { diff --git a/src/org/kde/kdeconnect/Device.java b/src/org/kde/kdeconnect/Device.java index 3da45204..cb234c8e 100644 --- a/src/org/kde/kdeconnect/Device.java +++ b/src/org/kde/kdeconnect/Device.java @@ -82,7 +82,8 @@ public class Device implements BaseLink.PackageReceiver { private List m_supportedPlugins = new ArrayList<>(); private final ConcurrentHashMap plugins = new ConcurrentHashMap<>(); private final ConcurrentHashMap failedPlugins = new ConcurrentHashMap<>(); - private Map> pluginsByIncomingInterface; + private final ConcurrentHashMap pluginsWithoutPermissions = new ConcurrentHashMap<>(); + private Map> pluginsByIncomingInterface = new HashMap<>(); private final SharedPreferences settings; @@ -722,6 +723,16 @@ public class Device implements BaseLink.PackageReceiver { failedPlugins.put(pluginKey, plugin); } + if(!plugin.checkRequiredPermissions()){ + Log.e("KDE/addPlugin", "No permission " + pluginKey); + plugins.remove(pluginKey); + pluginsWithoutPermissions.put(pluginKey, plugin); + success = false; + } else { + Log.i("KDE/addPlugin", "Permission OK " + pluginKey); + pluginsWithoutPermissions.remove(pluginKey); + } + return success; } @@ -812,6 +823,10 @@ public class Device implements BaseLink.PackageReceiver { return failedPlugins; } + public ConcurrentHashMap getPluginsWithoutPermissions() { + return pluginsWithoutPermissions; + } + public void addPluginsChangedListener(PluginsChangedListener listener) { pluginsChangedListeners.add(listener); } diff --git a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java index cfb17715..fa0c2b26 100644 --- a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java +++ b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/MousePadActivity.java @@ -97,11 +97,11 @@ public class MousePadActivity extends ActionBarActivity implements GestureDetect scrollDirection = 1; } String doubleTapSetting = prefs.getString(getString(R.string.mousepad_double_tap_key), - getString(R.string.mousepad_double_default)); + getString(R.string.mousepad_default_double)); String tripleTapSetting = prefs.getString(getString(R.string.mousepad_triple_tap_key), - getString(R.string.mousepad_triple_default)); + getString(R.string.mousepad_default_triple)); String sensitivitySetting = prefs.getString(getString(R.string.mousepad_sensitivity_key), - getString(R.string.mousepad_sensitivity_default)); + getString(R.string.mousepad_default_sensitivity)); doubleTapAction = ClickType.fromString(doubleTapSetting); tripleTapAction = ClickType.fromString(tripleTapSetting); diff --git a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java index 740cb2c5..3fb320f5 100644 --- a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java @@ -24,14 +24,18 @@ import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.app.Notification; +import android.app.PendingIntent; +import android.app.RemoteInput; import android.content.DialogInterface; import android.content.Intent; +import android.graphics.Bitmap; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.util.Log; + import org.kde.kdeconnect.Helpers.AppsHelper; import org.kde.kdeconnect.NetworkPackage; import org.kde.kdeconnect.Plugins.Plugin; @@ -39,15 +43,27 @@ import org.kde.kdeconnect.UserInterface.MaterialActivity; import org.kde.kdeconnect.UserInterface.SettingsActivity; import org.kde.kdeconnect_tp.R; +import java.io.ByteArrayOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) public class NotificationsPlugin extends Plugin implements NotificationReceiver.NotificationListener { public final static String PACKAGE_TYPE_NOTIFICATION = "kdeconnect.notification"; public final static String PACKAGE_TYPE_NOTIFICATION_REQUEST = "kdeconnect.notification.request"; + public final static String PACKAGE_TYPE_NOTIFICATION_REPLY = "kdeconnect.notification.reply"; + + + private boolean sendIcons = true; + + private Map pendingIntents; -/* - private boolean sendIcons = false; -*/ @Override public String getDisplayName() { @@ -81,6 +97,8 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver. @Override public boolean onCreate() { + pendingIntents = new HashMap(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { if (hasPermission()) { NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() { @@ -159,6 +177,8 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver. String packageName = statusBarNotification.getPackageName(); String appName = AppsHelper.appNameLookup(context, packageName); + + if ("com.facebook.orca".equals(packageName) && (statusBarNotification.getId() == 10012) && "Messenger".equals(appName) && @@ -182,28 +202,41 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver. np.set("silent", true); np.set("requestAnswer", true); //For compatibility with old desktop versions of KDE Connect that don't support "silent" } -/* + if (sendIcons) { try { - Drawable drawableAppIcon = AppsHelper.appIconLookup(context, packageName); - Bitmap appIcon = ImagesHelper.drawableToBitmap(drawableAppIcon); - ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - if (appIcon.getWidth() > 128) { - appIcon = Bitmap.createScaledBitmap(appIcon, 96, 96, true); + Bitmap appIcon = notification.largeIcon; + + if (appIcon != null) { + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + if (appIcon.getWidth() > 128) { + appIcon = Bitmap.createScaledBitmap(appIcon, 96, 96, true); + } + appIcon.compress(Bitmap.CompressFormat.PNG, 90, outStream); + byte[] bitmapData = outStream.toByteArray(); + + np.setPayload(bitmapData); + + np.set("payloadHash", getChecksum(bitmapData)); } - appIcon.compress(Bitmap.CompressFormat.PNG, 90, outStream); - byte[] bitmapData = outStream.toByteArray(); - np.setPayload(bitmapData); - } catch (Exception e) { + } catch(Exception e){ e.printStackTrace(); Log.e("NotificationsPlugin", "Error retrieving icon"); } } -*/ + + RepliableNotification rn = extractRepliableNotification(statusBarNotification); + if(rn.pendingIntent != null) { + np.set("requestReplyId", rn.id); + pendingIntents.put(rn.id, rn); + } + np.set("id", key); np.set("appName", appName == null? packageName : appName); np.set("isClearable", statusBarNotification.isClearable()); np.set("ticker", getTickerText(notification)); + np.set("title", getNotificationTitle(notification)); + np.set("text", getNotificationText(notification)); np.set("time", Long.toString(statusBarNotification.getPostTime())); if (requestAnswer) { np.set("requestAnswer", true); @@ -213,6 +246,127 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver. device.sendPackage(np); } + void replyToNotification(String id, String message){ + if(pendingIntents.isEmpty() || !pendingIntents.containsKey(id)){ + Log.e("NotificationsPlugin", "No such notification"); + return; + } + + RepliableNotification repliableNotification = pendingIntents.get(id); + if(repliableNotification == null) { + Log.e("NotificationsPlugin", "No such notification"); + return; + } + RemoteInput[] remoteInputs = new RemoteInput[repliableNotification.remoteInputs.size()]; + + Intent localIntent = new Intent(); + localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Bundle localBundle = new Bundle(); + int i = 0; + for(RemoteInput remoteIn : repliableNotification.remoteInputs){ + getDetailsOfNotification(remoteIn); + remoteInputs[i] = remoteIn; + localBundle.putCharSequence(remoteInputs[i].getResultKey(), message); + i++; + } + RemoteInput.addResultsToIntent(remoteInputs, localIntent, localBundle); + + try { + repliableNotification.pendingIntent.send(context, 0, localIntent); + } catch (PendingIntent.CanceledException e) { + Log.e("NotificationPlugin", "replyToNotification error: " + e.getMessage()); + } + pendingIntents.remove(id); + } + + private void getDetailsOfNotification(RemoteInput remoteInput) { + //Some more details of RemoteInput... no idea what for but maybe it will be useful at some point + String resultKey = remoteInput.getResultKey(); + String label = remoteInput.getLabel().toString(); + Boolean canFreeForm = remoteInput.getAllowFreeFormInput(); + if(remoteInput.getChoices() != null && remoteInput.getChoices().length > 0) { + String[] possibleChoices = new String[remoteInput.getChoices().length]; + for(int i = 0; i < remoteInput.getChoices().length; i++){ + possibleChoices[i] = remoteInput.getChoices()[i].toString(); + } + } + } + + private String getNotificationTitle(Notification notification) { + final String TITLE_KEY = "android.title"; + final String TEXT_KEY = "android.text"; + String title = ""; + + if(notification != null) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + try { + Bundle extras = notification.extras; + title = extras.getCharSequence(TITLE_KEY).toString(); + } catch(Exception e) { + Log.w("NotificationPlugin","problem parsing notification extras for " + notification.tickerText); + e.printStackTrace(); + } + } + } + + //TODO Add compat for under Kitkat devices + + return title; + } + + private RepliableNotification extractRepliableNotification(StatusBarNotification statusBarNotification) { + RepliableNotification repliableNotification = new RepliableNotification(); + + if(statusBarNotification != null) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + try { + Boolean reply = false; + + //works for WhatsApp, but not for Telegram + for(Notification.Action act : statusBarNotification.getNotification().actions) { + if(act != null && act.getRemoteInputs() != null) { + repliableNotification.remoteInputs.addAll(Arrays.asList(act.getRemoteInputs())); + repliableNotification.pendingIntent = act.actionIntent; + reply = true; + break; + } + } + + repliableNotification.packageName = statusBarNotification.getPackageName(); + + repliableNotification.tag = statusBarNotification.getTag();//TODO find how to pass Tag with sending PendingIntent, might fix Hangout problem + } catch(Exception e) { + Log.w("NotificationPlugin","problem extracting notification wear for " + statusBarNotification.getNotification().tickerText); + e.printStackTrace(); + } + } + } + + return repliableNotification; + } + + private String getNotificationText(Notification notification) { + final String TEXT_KEY = "android.text"; + String text = ""; + + if(notification != null) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + try { + Bundle extras = notification.extras; + Object extraTextExtra = extras.get(TEXT_KEY); + if (extraTextExtra != null) text = extraTextExtra.toString(); + } catch(Exception e) { + Log.w("NotificationPlugin","problem parsing notification extras for " + notification.tickerText); + e.printStackTrace(); + } + } + } + + //TODO Add compat for under Kitkat devices + + return text; + } + /** * Returns the ticker text of the notification. @@ -234,7 +388,7 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver. if (extraTextExtra != null) extraText = extraTextExtra.toString(); if (extraTitle != null && extraText != null && !extraText.isEmpty()) { - ticker = extraTitle + " ‐ " + extraText; + ticker = extraTitle + ": " + extraText; } else if (extraTitle != null) { ticker = extraTitle; } else if (extraText != null) { @@ -309,6 +463,10 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver. } }); + } else if (np.has("requestReplyId") && np.has("message")) { + + replyToNotification(np.getString("requestReplyId"), np.getString("message")); + } return true; @@ -352,7 +510,7 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver. @Override public String[] getSupportedPackageTypes() { - return new String[]{PACKAGE_TYPE_NOTIFICATION_REQUEST}; + return new String[]{PACKAGE_TYPE_NOTIFICATION_REQUEST,PACKAGE_TYPE_NOTIFICATION_REPLY}; } @Override @@ -402,4 +560,29 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver. } return result; } + + public String getChecksum(byte[] data){ + + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(data); + return bytesToHex(md.digest()); + } catch (NoSuchAlgorithmException e) { + Log.e("KDEConenct", "Error while generating checksum", e); + } + return null; + } + + + public static String bytesToHex(byte[] bytes) { + char[] hexArray = "0123456789ABCDEF".toCharArray(); + char[] hexChars = new char[bytes.length * 2]; + for ( int j = 0; j < bytes.length; j++ ) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars).toLowerCase(); + } + } diff --git a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/RepliableNotification.java b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/RepliableNotification.java new file mode 100644 index 00000000..f0dd1e56 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/RepliableNotification.java @@ -0,0 +1,16 @@ +package org.kde.kdeconnect.Plugins.NotificationsPlugin; + +import android.app.Notification; +import android.app.PendingIntent; +import android.os.Bundle; + +import java.util.ArrayList; +import java.util.UUID; + +public class RepliableNotification { + public String id = UUID.randomUUID().toString(); + public PendingIntent pendingIntent; + public ArrayList remoteInputs = new ArrayList<>(); + public String packageName; + public String tag; +} diff --git a/src/org/kde/kdeconnect/Plugins/Plugin.java b/src/org/kde/kdeconnect/Plugins/Plugin.java index 67650e7f..00b0c68b 100644 --- a/src/org/kde/kdeconnect/Plugins/Plugin.java +++ b/src/org/kde/kdeconnect/Plugins/Plugin.java @@ -20,23 +20,33 @@ package org.kde.kdeconnect.Plugins; +import android.Manifest; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; +import android.support.annotation.StringRes; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.util.Log; import android.view.View; import android.widget.Button; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.NetworkPackage; +import org.kde.kdeconnect.UserInterface.MaterialActivity; import org.kde.kdeconnect.UserInterface.PluginSettingsActivity; import org.kde.kdeconnect.UserInterface.SettingsActivity; +import org.kde.kdeconnect_tp.R; public abstract class Plugin { protected Device device; protected Context context; + protected int permissionExplanation = R.string.permission_explanation; public final void setContext(Context context, Device device) { this.device = device; @@ -167,14 +177,6 @@ public abstract class Plugin { */ public boolean onPackageReceived(NetworkPackage np) { return false; } - /** - * If onCreate returns false, should create a dialog explaining - * the problem (and how to fix it, if possible) to the user. - */ - public AlertDialog getErrorDialog(Activity deviceActivity) { - return null; - } - /** * Should return the list of NetworkPackage types that this plugin can handle */ @@ -205,4 +207,70 @@ public abstract class Plugin { return b; } + public String[] getRequiredPermissions() { + return new String[0]; + } + + public String[] getOptionalPermissions() { + return new String[0]; + } + + //Permission from Manifest.permission.* + protected boolean isPermissionGranted(String permission) { + int result = ContextCompat.checkSelfPermission(context, permission); + return (result == PackageManager.PERMISSION_GRANTED); + } + + protected boolean arePermissionsGranted(String[] permissions) { + for(String permission: permissions){ + if(!isPermissionGranted(permission)){ + return false; + } + } + return true; + } + + protected AlertDialog requestPermissionDialog(Activity activity, String permissions, @StringRes int reason) { + return requestPermissionDialog(activity, new String[]{permissions}, reason); + } + + protected AlertDialog requestPermissionDialog(final Activity activity, final String[] permissions, @StringRes int reason){ + return new AlertDialog.Builder(activity) + .setTitle(getDisplayName()) + .setMessage(reason) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + ActivityCompat.requestPermissions(activity, permissions, 0); + } + }) + .setNegativeButton(R.string.cancel,new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + //Do nothing + } + }) + .create(); + } + + /** + * If onCreate returns false, should create a dialog explaining + * the problem (and how to fix it, if possible) to the user. + */ + + public AlertDialog getErrorDialog(Activity deviceActivity) { + return null; + } + + public AlertDialog getPermissionExplanationDialog(Activity deviceActivity) { + return requestPermissionDialog(deviceActivity,getRequiredPermissions(), permissionExplanation); + } + + public boolean checkRequiredPermissions(){ + if (!arePermissionsGranted(getRequiredPermissions())) { + return false; + } + return true; + } + } diff --git a/src/org/kde/kdeconnect/Plugins/PluginFactory.java b/src/org/kde/kdeconnect/Plugins/PluginFactory.java index 826a5857..b514634a 100644 --- a/src/org/kde/kdeconnect/Plugins/PluginFactory.java +++ b/src/org/kde/kdeconnect/Plugins/PluginFactory.java @@ -125,8 +125,7 @@ public class PluginFactory { PluginFactory.registerPlugin(TelepathyPlugin.class); PluginFactory.registerPlugin(FindMyPhonePlugin.class); PluginFactory.registerPlugin(RunCommandPlugin.class); - //Commented here and in AndroidManifest until we release a desktop version with this feature, so we don't get bad "feature not working" reviews - //PluginFactory.registerPlugin(RemoteKeyboardPlugin.class); + PluginFactory.registerPlugin(RemoteKeyboardPlugin.class); } public static PluginInfo getPluginInfo(Context context, String pluginKey) { diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java index 59d33f63..65bad277 100644 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java @@ -20,8 +20,16 @@ package org.kde.kdeconnect.Plugins.SftpPlugin; +import android.Manifest; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.pm.PackageManager; import android.os.Environment; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.util.Log; +import org.json.JSONException; import org.kde.kdeconnect.Helpers.StorageHelper; import org.kde.kdeconnect.NetworkPackage; import org.kde.kdeconnect.Plugins.Plugin; @@ -38,6 +46,9 @@ public class SftpPlugin extends Plugin { private static final SimpleSftpServer server = new SimpleSftpServer(); + + + @Override public String getDisplayName() { return context.getResources().getString(R.string.pref_plugin_sftp); @@ -94,6 +105,7 @@ public class SftpPlugin extends Plugin { } else { res.append(context.getString(R.string.sftp_all_files)); } + String pathName = res.toString(); if (storage.readonly) { res.append(" "); @@ -127,6 +139,12 @@ public class SftpPlugin extends Plugin { return false; } + @Override + public String[] getRequiredPermissions() { + String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; + return perms; + } + @Override public String[] getSupportedPackageTypes() { return new String[]{PACKAGE_TYPE_SFTP_REQUEST}; diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/NotificationUpdateCallback.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/NotificationUpdateCallback.java index 78219de1..2b0a987e 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/NotificationUpdateCallback.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/NotificationUpdateCallback.java @@ -74,6 +74,9 @@ class NotificationUpdateCallback extends Device.SendPackageStatusCallback { public void onFailure(Throwable e) { updateDone(false); NotificationHelper.notifyCompat(notificationManager, notificationId, builder.build()); + if (e != null) { + e.printStackTrace(); + } } private void updateText() { diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java index c2fe9c79..ae94b465 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java @@ -23,8 +23,9 @@ package org.kde.kdeconnect.Plugins.SharePlugin; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.ActionBar; -import android.support.v7.app.ActionBarActivity; +import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; @@ -32,7 +33,6 @@ import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.ListView; - import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.NetworkPackage; @@ -46,15 +46,14 @@ import java.util.ArrayList; import java.util.Collection; -public class ShareActivity extends ActionBarActivity { +public class ShareActivity extends AppCompatActivity { - private MenuItem menuProgress; + private SwipeRefreshLayout mSwipeRefreshLayout; @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.refresh, menu); - menuProgress = menu.findItem(R.id.menu_progress); return true; } @@ -62,28 +61,7 @@ public class ShareActivity extends ActionBarActivity { public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.menu_refresh: - updateComputerList(); - BackgroundService.RunCommand(ShareActivity.this, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - service.onNetworkChange(); - } - }); - item.setVisible(false); - menuProgress.setVisible(true); - new Thread(new Runnable() { - @Override - public void run() { - try { Thread.sleep(1500); } catch (InterruptedException e) { } - runOnUiThread(new Runnable() { - @Override - public void run() { - menuProgress.setVisible(false); - item.setVisible(true); - } - }); - } - }).start(); + updateComputerListAction(); break; default: break; @@ -91,6 +69,30 @@ public class ShareActivity extends ActionBarActivity { return true; } + private void updateComputerListAction() { + updateComputerList(); + BackgroundService.RunCommand(ShareActivity.this, new BackgroundService.InstanceCallback() { + @Override + public void onServiceStart(BackgroundService service) { + service.onNetworkChange(); + } + }); + + mSwipeRefreshLayout.setRefreshing(true); + new Thread(new Runnable() { + @Override + public void run() { + try { Thread.sleep(1500); } catch (InterruptedException ignored) { } + runOnUiThread(new Runnable() { + @Override + public void run() { + mSwipeRefreshLayout.setRefreshing(false); + } + }); + } + }).start(); + } + private void updateComputerList() { final Intent intent = getIntent(); @@ -179,7 +181,6 @@ public class ShareActivity extends ActionBarActivity { device.sendPackage(np); } } - finish(); } }); @@ -193,13 +194,21 @@ public class ShareActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_list); + setContentView(R.layout.activity_refresh_list); ActionBar actionBar = getSupportActionBar(); - actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM); - - - setContentView(R.layout.activity_list); + mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.refresh_list_layout); + mSwipeRefreshLayout.setOnRefreshListener( + new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + updateComputerListAction(); + } + } + ); + if (actionBar != null) { + actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM); + } } diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java index fc2042e0..8be61db0 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java @@ -20,7 +20,9 @@ package org.kde.kdeconnect.Plugins.SharePlugin; +import android.Manifest; import android.app.Activity; +import android.app.AlertDialog; import android.app.DownloadManager; import android.app.Notification; import android.app.NotificationManager; @@ -30,6 +32,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.Cursor; import android.graphics.drawable.Drawable; @@ -110,6 +113,17 @@ public class SharePlugin extends Plugin { Log.i("SharePlugin", "hasPayload"); + int permissionCheck = ContextCompat.checkSelfPermission(context, + Manifest.permission.WRITE_EXTERNAL_STORAGE); + + if(permissionCheck == PackageManager.PERMISSION_GRANTED) { + + } else if(permissionCheck == PackageManager.PERMISSION_DENIED){ + // TODO Request Permission for storage + Log.i("SharePlugin", "no Permission for Storage"); + return false; + } + final InputStream input = np.getPayload(); final long fileLength = np.getPayloadSize(); final String originalFilename = np.getString("filename", Long.toString(System.currentTimeMillis())); @@ -132,6 +146,8 @@ public class SharePlugin extends Plugin { final OutputStream destinationOutput = context.getContentResolver().openOutputStream(destinationDocument.getUri()); final Uri destinationUri = destinationDocument.getUri(); + + final int notificationId = (int)System.currentTimeMillis(); Resources res = context.getResources(); final NotificationCompat.Builder builder = new NotificationCompat.Builder(context) @@ -159,10 +175,10 @@ public class SharePlugin extends Plugin { destinationOutput.write(data, 0, count); if (fileLength > 0) { if (progress >= fileLength) break; - long progressPercentage = (progress * 100 / fileLength); + long progressPercentage = (progress * 10 / fileLength); if (progressPercentage != prevProgressPercentage) { prevProgressPercentage = progressPercentage; - builder.setProgress(100, (int) progressPercentage, false); + builder.setProgress(100, (int) progressPercentage*10, false); NotificationHelper.notifyCompat(notificationManager, notificationId, builder.build()); } } @@ -186,25 +202,33 @@ public class SharePlugin extends Plugin { //Update the notification and allow to open the file from it Resources res = context.getResources(); String message = successful? res.getString(R.string.received_file_title, device.getName()) : res.getString(R.string.received_file_fail_title, device.getName()); - NotificationCompat.Builder builder = new NotificationCompat.Builder(context) - .setContentTitle(message) + builder.setContentTitle(message) .setTicker(message) .setSmallIcon(android.R.drawable.stat_sys_download_done) - .setAutoCancel(true); - if (successful) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(destinationUri, mimeType); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); - stackBuilder.addNextIntent(intent); - PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); - builder.setContentText(res.getString(R.string.received_file_text, destinationDocument.getName())) - .setContentIntent(resultPendingIntent); + .setAutoCancel(true) + .setProgress(100,100,false) + .setOngoing(false); + + // Nougat requires share:// URIs instead of file:// URIs + // TODO use FileProvider for >Nougat + if(Build.VERSION.SDK_INT < 24) { + if (successful) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(destinationUri, mimeType); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); + stackBuilder.addNextIntent(intent); + PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + builder.setContentText(res.getString(R.string.received_file_text, destinationDocument.getName())) + .setContentIntent(resultPendingIntent); + } } + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if (prefs.getBoolean("share_notification_preference", true)) { builder.setDefaults(Notification.DEFAULT_ALL); } + NotificationHelper.notifyCompat(notificationManager, notificationId, builder.build()); if (successful) { @@ -408,5 +432,10 @@ public class SharePlugin extends Plugin { return new String[]{PACKAGE_TYPE_SHARE_REQUEST}; } + @Override + public String[] getRequiredPermissions() { + String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; + return perms; + } } diff --git a/src/org/kde/kdeconnect/Plugins/TelepathyPlugin/TelepathyPlugin.java b/src/org/kde/kdeconnect/Plugins/TelepathyPlugin/TelepathyPlugin.java index 67a5ce86..55f5b88c 100644 --- a/src/org/kde/kdeconnect/Plugins/TelepathyPlugin/TelepathyPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/TelepathyPlugin/TelepathyPlugin.java @@ -20,6 +20,10 @@ package org.kde.kdeconnect.Plugins.TelepathyPlugin; +import android.Manifest; +import android.content.pm.PackageManager; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; import android.telephony.SmsManager; import android.util.Log; @@ -45,11 +49,6 @@ public class TelepathyPlugin extends Plugin { return context.getResources().getString(R.string.pref_plugin_telepathy_desc); } - @Override - public boolean onCreate() { - return true; - } - @Override public void onDestroy() { } @@ -65,6 +64,10 @@ public class TelepathyPlugin extends Plugin { String phoneNo = np.getString("phoneNumber"); String sms = np.getString("messageBody"); try { + int permissionCheck = ContextCompat.checkSelfPermission(context, + Manifest.permission.SEND_SMS); + + if(permissionCheck == PackageManager.PERMISSION_GRANTED) { SmsManager smsManager = SmsManager.getDefault(); ArrayList parts = smsManager.divideMessage(sms); @@ -72,7 +75,9 @@ public class TelepathyPlugin extends Plugin { // If this message turns out to fit in a single SMS, sendMultpartTextMessage // properly handles that case smsManager.sendMultipartTextMessage(phoneNo, null, parts, null, null); - + } else if(permissionCheck == PackageManager.PERMISSION_DENIED){ + // TODO Request Permission SEND_SMS + } //TODO: Notify other end } catch (Exception e) { //TODO: Notify other end @@ -184,4 +189,8 @@ public class TelepathyPlugin extends Plugin { return new String[]{}; } + @Override + public String[] getRequiredPermissions() { + return new String[]{Manifest.permission.SEND_SMS/*, Manifest.permission.READ_CONTACTS*/}; + } } diff --git a/src/org/kde/kdeconnect/Plugins/TelephonyPlugin/TelephonyPlugin.java b/src/org/kde/kdeconnect/Plugins/TelephonyPlugin/TelephonyPlugin.java index 27d3af74..33a55cde 100644 --- a/src/org/kde/kdeconnect/Plugins/TelephonyPlugin/TelephonyPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/TelephonyPlugin/TelephonyPlugin.java @@ -20,13 +20,16 @@ package org.kde.kdeconnect.Plugins.TelephonyPlugin; +import android.Manifest; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.media.AudioManager; import android.os.Build; import android.os.Bundle; +import android.support.v4.content.ContextCompat; import android.telephony.SmsMessage; import android.telephony.TelephonyManager; import android.util.Log; @@ -112,32 +115,40 @@ public class TelephonyPlugin extends Plugin { //Log.e("TelephonyPlugin", "callBroadcastReceived"); - Map contactInfo = ContactsHelper.phoneNumberLookup(context, phoneNumber); NetworkPackage np = new NetworkPackage(PACKAGE_TYPE_TELEPHONY); - if (phoneNumber != null) { - np.set("phoneNumber", phoneNumber); - } + int permissionCheck = ContextCompat.checkSelfPermission(context, + Manifest.permission.READ_CONTACTS); + + if(permissionCheck==PackageManager.PERMISSION_GRANTED) { + + Map contactInfo = ContactsHelper.phoneNumberLookup(context, phoneNumber); + + if (contactInfo.containsKey("name")) { + np.set("contactName", contactInfo.get("name")); + } + + if (contactInfo.containsKey("photoID")) { + String photoUri = contactInfo.get("photoID"); + if (photoUri != null) { + try { + String base64photo = ContactsHelper.photoId64Encoded(context, photoUri); + if (base64photo != null && !base64photo.isEmpty()) { + np.set("phoneThumbnail", base64photo); + } + } catch (Exception e) { + Log.e("TelephonyPlugin", "Failed to get contact photo"); + } + } + + } - if (contactInfo.containsKey("name")) { - np.set("contactName", contactInfo.get("name")); } else { np.set("contactName", phoneNumber); } - if (contactInfo.containsKey("photoID")) { - String photoUri = contactInfo.get("photoID"); - if (photoUri != null) { - try { - String base64photo = ContactsHelper.photoId64Encoded(context, photoUri); - if (base64photo != null && !base64photo.isEmpty()) { - np.set("phoneThumbnail", base64photo); - } - } catch (Exception e) { - Log.e("TelephonyPlugin", "Failed to get contact photo"); - } - } - + if (phoneNumber != null) { + np.set("phoneNumber", phoneNumber); } switch (state) { @@ -231,18 +242,26 @@ public class TelephonyPlugin extends Plugin { } String phoneNumber = messages.get(0).getOriginatingAddress(); - Map contactInfo = ContactsHelper.phoneNumberLookup(context, phoneNumber); + + int permissionCheck = ContextCompat.checkSelfPermission(context, + Manifest.permission.READ_CONTACTS); + + if(permissionCheck==PackageManager.PERMISSION_GRANTED) { + Map contactInfo = ContactsHelper.phoneNumberLookup(context, phoneNumber); + + if (contactInfo.containsKey("name")) { + np.set("contactName", contactInfo.get("name")); + } + + if (contactInfo.containsKey("photoID")) { + np.set("phoneThumbnail", ContactsHelper.photoId64Encoded(context, contactInfo.get("photoID"))); + } + } if (phoneNumber != null) { np.set("phoneNumber", phoneNumber); } - if (contactInfo.containsKey("name")) { - np.set("contactName", contactInfo.get("name")); - } - if (contactInfo.containsKey("photoID")) { - np.set("phoneThumbnail", ContactsHelper.photoId64Encoded(context, contactInfo.get("photoID"))); - } device.sendPackage(np); } @@ -290,4 +309,9 @@ public class TelephonyPlugin extends Plugin { return new String[]{PACKAGE_TYPE_TELEPHONY}; } + @Override + public String[] getRequiredPermissions() { + return new String[]{Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_CONTACTS, Manifest.permission.SEND_SMS}; + } + } diff --git a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java index 56d2c8b8..a5435f7f 100644 --- a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java +++ b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java @@ -68,6 +68,7 @@ public class DeviceFragment extends Fragment { Device device; TextView errorHeader; + TextView noPermissionsHeader; MaterialActivity mActivity; @@ -389,6 +390,38 @@ public class DeviceFragment extends Fragment { } } + //Plugins without permissions List + final ConcurrentHashMap permissionsNeeded = device.getPluginsWithoutPermissions(); + if (!permissionsNeeded.isEmpty()) { + if (noPermissionsHeader == null) { + noPermissionsHeader = new TextView(mActivity); + noPermissionsHeader.setPadding( + 0, + ((int) (28 * getResources().getDisplayMetrics().density)), + 0, + ((int) (8 * getResources().getDisplayMetrics().density)) + ); + noPermissionsHeader.setOnClickListener(null); + noPermissionsHeader.setOnLongClickListener(null); + noPermissionsHeader.setText(getResources().getString(R.string.plugins_need_permission)); + } + items.add(new CustomItem(noPermissionsHeader)); + for (Map.Entry entry : permissionsNeeded.entrySet()) { + String pluginKey = entry.getKey(); + final Plugin plugin = entry.getValue(); + if (plugin == null) { + items.add(new SmallEntryItem(pluginKey)); + } else { + items.add(new SmallEntryItem(plugin.getDisplayName(), new View.OnClickListener() { + @Override + public void onClick(View v) { + plugin.getPermissionExplanationDialog(mActivity).show(); + } + })); + } + } + } + ListView buttonsList = (ListView) rootView.findViewById(R.id.buttons_list); ListAdapter adapter = new ListAdapter(mActivity, items); buttonsList.setAdapter(adapter); diff --git a/src/org/kde/kdeconnect/UserInterface/MaterialActivity.java b/src/org/kde/kdeconnect/UserInterface/MaterialActivity.java index 4eeed193..d8201032 100644 --- a/src/org/kde/kdeconnect/UserInterface/MaterialActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/MaterialActivity.java @@ -1,14 +1,17 @@ package org.kde.kdeconnect.UserInterface; +import android.Manifest; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.design.widget.NavigationView; +import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; @@ -294,6 +297,22 @@ public class MaterialActivity extends AppCompatActivity { } } + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + for (int result : grantResults) { + if (result == PackageManager.PERMISSION_GRANTED) { + //New permission granted, reload plugins + BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { + @Override + public void onServiceStart(BackgroundService service) { + Device device = service.getDevice(mCurrentDevice); + device.reloadPluginsFromSettings(); + } + }); + } + } + } + public void renameDevice() { final TextView nameView = (TextView) mNavigationView.findViewById(R.id.device_name); final EditText deviceNameEdit = new EditText(MaterialActivity.this); diff --git a/src/org/kde/kdeconnect/UserInterface/PairingFragment.java b/src/org/kde/kdeconnect/UserInterface/PairingFragment.java index b848baf7..67c37543 100644 --- a/src/org/kde/kdeconnect/UserInterface/PairingFragment.java +++ b/src/org/kde/kdeconnect/UserInterface/PairingFragment.java @@ -24,6 +24,7 @@ import android.app.Activity; import android.content.Intent; import android.content.res.Resources; import android.os.Bundle; +import android.support.v4.widget.SwipeRefreshLayout; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.Menu; @@ -55,10 +56,10 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb private static final int RESULT_PAIRING_SUCCESFUL = Activity.RESULT_FIRST_USER; private View rootView; + private View listRootView; + private SwipeRefreshLayout mSwipeRefreshLayout; private MaterialActivity mActivity; - private MenuItem menuProgress; - boolean listRefreshCalledThisFrame = false; TextView headerText; @@ -75,12 +76,21 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb setHasOptionsMenu(true); - rootView = inflater.inflate(R.layout.activity_list, container, false); - + rootView = inflater.inflate(R.layout.activity_refresh_list, container, false); + listRootView = rootView.findViewById(R.id.listView1); + mSwipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.refresh_list_layout); + mSwipeRefreshLayout.setOnRefreshListener( + new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + updateComputerListAction(); + } + } + ); headerText = new TextView(inflater.getContext()); headerText.setText(getString(R.string.pairing_description)); headerText.setPadding(0, (int) (16 * getResources().getDisplayMetrics().density), 0, (int) (12 * getResources().getDisplayMetrics().density)); - ((ListView) rootView).addHeaderView(headerText); + ((ListView) listRootView).addHeaderView(headerText); return rootView; } @@ -91,7 +101,30 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb mActivity = ((MaterialActivity) getActivity()); } - void updateComputerList() { + private void updateComputerListAction() { + updateComputerList(); + BackgroundService.RunCommand(mActivity, new BackgroundService.InstanceCallback() { + @Override + public void onServiceStart(BackgroundService service) { + service.onNetworkChange(); + } + }); + mSwipeRefreshLayout.setRefreshing(true); + new Thread(new Runnable() { + @Override + public void run() { + try { Thread.sleep(1500); } catch (InterruptedException ignored) { } + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + mSwipeRefreshLayout.setRefreshing(false); + } + }); + } + }).start(); + } + + private void updateComputerList() { BackgroundService.RunCommand(mActivity, new BackgroundService.InstanceCallback() { @Override public void onServiceStart(final BackgroundService service) { @@ -232,33 +265,13 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.pairing, menu); - menuProgress = menu.findItem(R.id.menu_progress); } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.menu_refresh: - updateComputerList(); - BackgroundService.RunCommand(mActivity, new BackgroundService.InstanceCallback() { - @Override - public void onServiceStart(BackgroundService service) { - service.onNetworkChange(); - } - }); - menuProgress.setVisible(true); - new Thread(new Runnable() { - @Override - public void run() { - try { Thread.sleep(1500); } catch (InterruptedException e) { } - mActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - menuProgress.setVisible(false); - } - }); - } - }).start(); + updateComputerListAction(); break; case R.id.menu_rename: mActivity.renameDevice();