From 4cb157fe72b3628546e3a80b65b3236d9a00ea86 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Fri, 19 Aug 2022 11:31:42 +0600 Subject: [PATCH 001/285] Update applicationId to foundation.e.accountmanager --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index bdb6d3118..49f28aeef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,7 +13,7 @@ android { buildToolsVersion '32.0.0' defaultConfig { - applicationId "at.bitfire.davdroid" + applicationId "foundation.e.accountmanager" versionCode 402020000 versionName '4.2.2' -- GitLab From 85977be99d4be099878fc309cbb4efb987d26b6a Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Fri, 19 Aug 2022 11:35:29 +0600 Subject: [PATCH 002/285] build.gradle: set minSdkVersion to 24(Android 7.1) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 49f28aeef..012ff381e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,7 +21,7 @@ android { setProperty "archivesBaseName", "davx5-ose-" + getVersionName() - minSdkVersion 21 // Android 5 + minSdkVersion 24 // Android 7.1 targetSdkVersion 32 // Android 12v2 buildConfigField "String", "userAgent", "\"DAVx5\"" -- GitLab From 96cbb72fc95fb0e21435f7c990dd7aea5c52e469 Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Thu, 7 Jun 2018 12:58:14 +0530 Subject: [PATCH 003/285] Created new account types for Google and eelo accounts. --- app/src/main/AndroidManifest.xml | 26 +++ .../EeloAccountAuthenticatorService.kt | 155 ++++++++++++++++++ .../GoogleAccountAuthenticatorService.kt | 154 +++++++++++++++++ .../ic_account_provider_eelo.png | Bin 0 -> 17628 bytes .../ic_account_provider_google.png | Bin 0 -> 21709 bytes app/src/main/res/values/strings.xml | 8 +- .../res/xml/eelo_account_authenticator.xml | 17 ++ .../res/xml/google_account_authenticator.xml | 16 ++ 8 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt create mode 100644 app/src/main/res/drawable-xxhdpi/ic_account_provider_eelo.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_account_provider_google.png create mode 100644 app/src/main/res/xml/eelo_account_authenticator.xml create mode 100644 app/src/main/res/xml/google_account_authenticator.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 957d80859..5c69b7e8e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -259,6 +259,32 @@ + + + + + + + + + + + + + + + + + + . + */ + +package at.bitfire.davdroid.syncadapter + +import android.accounts.* +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Bundle +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.ui.setup.LoginActivity +import dagger.hilt.android.AndroidEntryPoint +import java.util.logging.Level +import javax.inject.Inject +import kotlin.concurrent.thread + +/** + * Account authenticator for the eelo account type. + * + * Gets started when an eelo account is removed, too, so it also watches for account removals + * and contains the corresponding cleanup code. + */ + +@AndroidEntryPoint +class EeloAccountAuthenticatorService : Service(), OnAccountsUpdateListener { + + companion object { + + fun cleanupAccounts(context: Context, db: AppDatabase) { + Logger.log.info("Cleaning up orphaned accounts") + + val accountManager = AccountManager.get(context) + val accountNames = + accountManager.getAccountsByType(context.getString(R.string.account_type)) + .map { it.name } + + // delete orphaned address book accounts + accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)) + .map { LocalAddressBook(context, it, null) } + .forEach { + try { + if (!accountNames.contains(it.mainAccount.name)) + it.delete() + } catch (e: Exception) { + Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e) + } + } + + // delete orphaned services in DB + val serviceDao = db.serviceDao() + if (accountNames.isEmpty()) + serviceDao.deleteAll() + else + serviceDao.deleteExceptAccounts(accountNames.toTypedArray()) + } + + } + + @Inject + lateinit var db: AppDatabase + + private lateinit var accountManager: AccountManager + private lateinit var accountAuthenticator: AccountAuthenticator + + override fun onCreate() { + accountManager = AccountManager.get(this) + accountManager.addOnAccountsUpdatedListener(this, null, true) + + accountAuthenticator = AccountAuthenticator(this) + } + + override fun onDestroy() { + super.onDestroy() + accountManager.removeOnAccountsUpdatedListener(this) + } + + override fun onBind(intent: Intent?) = + accountAuthenticator.iBinder.takeIf { intent?.action == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT } + + + override fun onAccountsUpdated(accounts: Array?) { + thread { + cleanupAccounts(this, db) + } + } + + + private class AccountAuthenticator( + val context: Context + ) : AbstractAccountAuthenticator(context) { + + override fun addAccount( + response: AccountAuthenticatorResponse?, + accountType: String?, + authTokenType: String?, + requiredFeatures: Array?, + options: Bundle? + ): Bundle { + val intent = Intent(context, LoginActivity::class.java) + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response) + val bundle = Bundle(1) + bundle.putParcelable(AccountManager.KEY_INTENT, intent) + return bundle + } + + override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = + null + + override fun getAuthTokenLabel(p0: String?) = null + override fun confirmCredentials( + p0: AccountAuthenticatorResponse?, + p1: Account?, + p2: Bundle? + ) = null + + override fun updateCredentials( + p0: AccountAuthenticatorResponse?, + p1: Account?, + p2: String?, + p3: Bundle? + ) = null + + override fun getAuthToken( + p0: AccountAuthenticatorResponse?, + p1: Account?, + p2: String?, + p3: Bundle? + ) = null + + override fun hasFeatures( + p0: AccountAuthenticatorResponse?, + p1: Account?, + p2: Array? + ) = null + + } +} + diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt new file mode 100644 index 000000000..b25532ad1 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt @@ -0,0 +1,154 @@ +/* + * Copyright ECORP SAS 2022 + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 at.bitfire.davdroid.syncadapter + +import android.accounts.* +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Bundle +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.ui.setup.LoginActivity +import dagger.hilt.android.AndroidEntryPoint +import java.util.logging.Level +import javax.inject.Inject +import kotlin.concurrent.thread + +/** + * Account authenticator for the Google account type. + * + * Gets started when a Google account is removed, too, so it also watches for account removals + * and contains the corresponding cleanup code. + */ + +@AndroidEntryPoint +class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener { + + companion object { + fun cleanupAccounts(context: Context, db: AppDatabase) { + Logger.log.info("Cleaning up orphaned accounts") + + val accountManager = AccountManager.get(context) + val accountNames = + accountManager.getAccountsByType(context.getString(R.string.account_type)) + .map { it.name } + + // delete orphaned address book accounts + accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)) + .map { LocalAddressBook(context, it, null) } + .forEach { + try { + if (!accountNames.contains(it.mainAccount.name)) + it.delete() + } catch (e: Exception) { + Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e) + } + } + + // delete orphaned services in DB + val serviceDao = db.serviceDao() + if (accountNames.isEmpty()) + serviceDao.deleteAll() + else + serviceDao.deleteExceptAccounts(accountNames.toTypedArray()) + } + + } + + @Inject + lateinit var db: AppDatabase + + private lateinit var accountManager: AccountManager + private lateinit var accountAuthenticator: AccountAuthenticator + + override fun onCreate() { + accountManager = AccountManager.get(this) + accountManager.addOnAccountsUpdatedListener(this, null, true) + + accountAuthenticator = AccountAuthenticator(this) + } + + override fun onDestroy() { + super.onDestroy() + accountManager.removeOnAccountsUpdatedListener(this) + } + + override fun onBind(intent: Intent?) = + accountAuthenticator.iBinder.takeIf { intent?.action == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT } + + + override fun onAccountsUpdated(accounts: Array?) { + thread { + cleanupAccounts(this, db) + } + } + + + private class AccountAuthenticator( + val context: Context + ) : AbstractAccountAuthenticator(context) { + + override fun addAccount( + response: AccountAuthenticatorResponse?, + accountType: String?, + authTokenType: String?, + requiredFeatures: Array?, + options: Bundle? + ): Bundle { + val intent = Intent(context, LoginActivity::class.java) + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response) + val bundle = Bundle(1) + bundle.putParcelable(AccountManager.KEY_INTENT, intent) + return bundle + } + + override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = + null + + override fun getAuthTokenLabel(p0: String?) = null + override fun confirmCredentials( + p0: AccountAuthenticatorResponse?, + p1: Account?, + p2: Bundle? + ) = null + + override fun updateCredentials( + p0: AccountAuthenticatorResponse?, + p1: Account?, + p2: String?, + p3: Bundle? + ) = null + + override fun getAuthToken( + p0: AccountAuthenticatorResponse?, + p1: Account?, + p2: String?, + p3: Bundle? + ) = null + + override fun hasFeatures( + p0: AccountAuthenticatorResponse?, + p1: Account?, + p2: Array? + ) = null + + } +} + diff --git a/app/src/main/res/drawable-xxhdpi/ic_account_provider_eelo.png b/app/src/main/res/drawable-xxhdpi/ic_account_provider_eelo.png new file mode 100644 index 0000000000000000000000000000000000000000..435feabb03767ec8b09e320225926ffdf44990e2 GIT binary patch literal 17628 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 P00000NkvXXu0mjf`8mYM literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_account_provider_google.png b/app/src/main/res/drawable-xxhdpi/ic_account_provider_google.png new file mode 100644 index 0000000000000000000000000000000000000000..2154694f964da71ead68a6ff4baa784a21e76a03 GIT binary patch literal 21709 zcmXtAWmHsM8@&Sz9Yc46w3L)|N~m;qBOypPLnBhs-AZ?dq;z+~AQIBu@y+|=o3&={ ztod`#I(I*@pS{m~Rr&Z13!Mxd001lnc^NhMwd=n(Dl+_2ZPgwH0B8UO8A%OKqa#C< zOajg6s$&K6#n9+3#YIM@Av}>jqzHM4X!l`>)1FthC$fsFL?2+qbwDaMr9SFRhl27` zSk~e@gQSkL5z%71a9*deo5xkV|BUDwuP|?0NDF!R(!bl*p3Y~@s%)Uhs7MaRKn2=l z=(tgD^0!5TB_d?v1toYReProUc)C?ydt?C%G9ofvp=PQ<6YkG)TJO}+A>yQ5cSb+UG1cVZDR1;TsPAUH)x|d1HO*u< zH{BKU0$F!%b%yrz^c*I0=r<}WD173KG_wou{SE@6!IE7bt;~`CfVh|!WGKZ|VK{>V z>-PEp088CC5&v`L?J++)YrkIqY39T_%T0?L$UVJ?G`U_I_sgnZfv@p2Hk;UKRgdWM zm(Pv6va<5YzLVgxFhz{}ElZ@CK461q{ps!g%|jCsLLi5K=gZCV@-l~|rKO!-z0DbU z0m&u-!+45^FXeM0LWmfrZr7XuT6?qgM0O=9Dd}#rL8VLP$6acYg9^&-lN-biu;io| zC3g5@+r%!SLOcxwb+B^_qjQVAsayHZB8VnnfY{&rm(iW$C6TP^L)I@>h4{!P|Cbv+ zKE4e~QSZwP49@n`rmKU61iI>+^O9Ij?&yuVY&F2rUNaR8rjrHYWlZcpf zTy3uT;P3w&{kiEuQ4*q8JAoC7Q^AeiwsI7V2~yAK#U2(Tor#u!^LsayryHvtjnB zI~w+}JvJ5{x*jNkh}diEC$`ZuJw5H^vNNhH=y!ifhG-cPUC1|CkhzDRi~rbm+&aSR zuv}lJuC9JUfRu+QnvkAOM!FnF7(G`PInVoBPXYDk=9eL98Vf~5#p!k_8pxu7me%zw zeXI!2$q-Xcxu`C|ri>;)IhrQmXjCv&@GcgP))qrZ$c#$Y6kWbaZiBUHaN1ywbRi+{DIDrAH8nNg&iC!JoGNxZQdj(lePb0N_i2v;sqXgVFX1V^C<1sQo*_vC z{zNXHZZ=&zhnM&z0<3!8mE5Z_oPI z2UgBlRQi>ph-bBREiJe4>)_bpg>RT`J*`2{*s%Yby$xj~@FWto%R5F!#uWZf(-uEz zP{DhDaQA6OB}bJoKelJf*Jsm_jZoQbn~5Sao)6L&?=HLFo^C$4I6I4JCPA%Z{_vH5 z5j{RD2iRA?9Im$1`CROb<$QUYWsTckRVf$z!$KP$Bw)soc;zL+?% zIQb+kzmJ?nn4wlzs<$}QVj`@(ycdMPphUN1X6$?pF&*2j(=cxc5o}5G6`AlQG6{9@ z1Er|vDQs*ko$n!26#+dFpJ@s=yCjbd;1>B~{CrM4F{3R0R6nql2EB=JwX(EiZ25!g zC|o;MVCg}pDOGlKWa4iWabWhZgf5A_bIRk5*wE0BUYvoU;nO}Ge<#@jj#NC5VgT#y z*ZDX?iC__UNIR_jP)hMa@`{@&qHC}T>971D+>q5FvtlE1Dm3{B)<9bA-@SNhwVf-q zyT3YU`jID{A65*!9d!9S_`NLs&8Kq_;7vaI2SqDctAVXfH#j(Oxq%b3LvQplRr6m6 z$o;9pDunrulX=n!&Mq#yd=~PvlLSyge``4Yw&2Ta+LF!G_HGIMitj*Lu98>OtEk9NI6C|E(P4$SDeXfF;;Ysrs)6O=nP z-P>Lwf}=fic4n4qE~ik(SjvqBaDpa=Y*x8H$+4?`_%Jpgu;F`g+qCYDaRL5|wr_|= z7KC8a^B*pY*4$Sda^7Z*OA6)^5)$%^^!|Cz_I^3eU*LiOOH$#;8(nzo)3xtlb>YvS z!xK{nSN`Jg95h1Zb67JShDWi`gb;TsEcSTXYh~Uab@%(9>$Rl8*GDmI`*K*AW_B(u z>h{_G*HIC`M|==ca*?bp_H;Q@@Jcy5igDily$WX*>%KbZ&E|h8q)ajxnFCL@21~!n zap0c8EJKIIv@mt!W9@4__W^rZH!POdhf6Q;DE~$NcEE99X0h+lo!zHAR8&vd^rqv;K73`; zg1k`SmyzQp9Esr_7vsW&S3i)xY5^Z^Cq!=yZt`Wn?X;~Oo0e}^aW;QpMK0ENP+`~# zUokU@;+BLzh#r@5q20*skreZHks)=`Up0W_T@Oud?are!&u32_^Jn?ZEK1sM$h})) z-}{lu`>d-*Dw#}@a!X71dlM5AGpch1rWE6t!MT{6{J&h2lafT2i&YC01W^SgMe!O3 zVj#cC#AMt=8r+3p7lBZFEloyX-OhpUXM<#Zr@6jNp|>oqOc4e{3dZk5g1C4!iLT?% zi<62+l##)Ukduv%m?o}ksNr0%y3RN_IQ%NVZ1vxisYGB!9qnsqtT0M}T(_|XVkk2* zv$Mr0s~X$eAAfJ&Y0@T#x}AJJtun+4+z^m58Ig-~cR=i>?pk7~l?Z0e9nw6{tJR># zp))lVg%WhBX~#W^LDF{u7aQp#DBlK8mS99=wL)nkXzm_qR9 zu_5x0_I;iyCb(Vu2Mm5jH(UP%fRDPDei3(lsvwBXbN>+|gO-18j~wk=AtKQXPz3bq z02fF$$nzEY0{8GRGA{KFYF@zZ(+8p62yX z;r8*6hBfiQj2jS4Mqc%mnk%`~kJr-F{mgnIv)ZQrY5?+Zzr>^^4nd~{(PR!gV4IzE z!1m^-LxLJ<-h8HGQz}edxAl3(vw5(o5DySx&*)N|mY{LkyIL@&7X0$(_pd0t-xbE6 zLY@g|up=>Zs=tExZi?e7OEwG>wz*@J6VPGAl|W}^kE*5gLXeTU^}v28D3aI+3a2+6P;SBx z`TVQfZ7iDR%?KhWFEOw$CTW`q6~Amt6=Vpl3NVk zp7L_oc@_&dZ>DTE7o(=o!8yOS^BC z;U<1J;r-+L7Wg{U2=uaZ4V~;j6ddnF&Gfqu4q5-I3#HGT+Yt?%OC16yuzezJ=;P@Z@V0=bRq&(62AKH;W|`%d|rkn1f?tz4}j=y!-WI#xN#$zFgNu zA(l#%iz-52g#riMWlp#DPp8^MGGnth{3Qb8x9TLh(l^}ir7KG$#I1`>D85w_;7=2; z(<&3sezE$d!|DRd^H8`VK{`sGH$jVQnj;k(!7+W_sj!}RJR)?lff?w4nH7=X=Tr|(Wh(|Y6?zzPx`SRBW1Sba5*o(uj(Z0P!Z z_Zw%&?L0h_7cLnbnxt9!9_hsCn0`DG%tw6=exR*}siQt#`axp$K9rd)Sz=6}G!9b} z=vs1!+og})NTbd5OKZNK7Q)&pv;R)FO2UhTXefHK~3sz~&C} zZE{=OI21BVL*kJZ%zL$dCc#b!{ovk$O)ciDY-P3chJxnL1IJtq6~6@Y=F6u3Y3D<$ zv%65Wr8b4-VjATA_>v`Qg3F4XaWo0doXE_b;DBVas3(yX;ymx^Lz+P+Ubqq<#wl3Y zfkr_bjr?6Vmot4cwgJT?&q+@&9a9*GO61mQ=I00N0eY&4286-1^_z7c&X-8GN*!8M zDf*tLn*Q6zGfyRptG3ZH+Ep(%YffPhqMR~mBA`xmXp8?c<1B#~5-mOuCEvl{EVTWIT^YLwRSLTaO;(Mt`b z0@7#|;)jg`o~}Rv&lfpO18=D!k`QbrQGn|4UBuV{UW$Jo)5l4o-M-F=iL6RVUccPv zpZeVfIC}*p7t^iU4{w#>5jdN&Kcam=45?Hc)^oP<`CwNP)AJjjCBDcIOrFnd+l#b( zxXEIn;-)*!XhIxb<_PTG5wo^{uPpnJalvF|))uZ5A}Alq%FD|i-@=&+dOwCreEG)| z2pd>p6*|As@OiCu)3D7wYohU$bxn4XW#j#?%2Zy+Y4FS45=2KG6)JiT_*CqQp#q0j4=PX@v!0s1bAM zUE)_}uB()b+fgB)+ytLca|UVDgC#VG3g$a?zK!TQOY^_7sSO5koq*m~i}#@)PW(K? znP4TY6aQKB%{z@G-v+Y+`xW?aA*t&=`tiy!h*sL@{lGaP=i@{+2`Q~@_uiFvQ>M>x zOTg{M%X7Gr04^VEf$qpRzNOqs^ju4yse58#V)4|ZBsU!Ar`e2+sH05|zru|p9p-ihLr_r|!q7s>?()pV^U@}A#bHg;s$<&PL8IYto zWgA$mCGUn6`A&sKTRu$Znql8BwI4usd*LZ#4TQu=C@O#^{9>6c30MEDJ$kfJbzy4h zCK*?EnpZ$rdjb(rs6Ne?lafPbBKcJRNw5=0go&NBUwnLSSTJfs2SfxbxN+yZ=PBv? zTK0)g$GoJ5{_uRWM7qH-zY9(u%8X&JJ!c|ljaZ_t0LGcEVCGokh&IE;*~X&6e)P>U zVXEJ7XMca$J4qB!8E4Z>3f?VQkQj6zY}qCj8Q^E+{ZhX;2eV*6}4*6!Kf}rL0a-U8)AA}#E zJU*985y(^bc4Xq{9aS&0l}VX|G*1g_zkpdlj~5hj9bUo0b4p_W;d`=ba+lka41|;9 zr)r_qmi+=^$O?6==>R?rL60Y}@h+CikBOds%h8s->(!wBl8k#nD>DG@7;kaeZEU}q z*Iz(E0NGpH2$qQcVJq9hw&6t-e*!w1jJ4GA!Z>ToOThvor(v*{Tv`5s=UPuE>urvf zFK8-pn`pGRNn2m2Xgu+LF-<=bj67E>=37M(>ijUMz_>u{mD7vyr>hXRlbOOm4N?)c zn=kKB|M|YV63?xaFj5Pob8zySg~f{pzOsq$7Vu6Yc<#j190-@LT)mtK(B5f_<`y5h zIN1B1TO=_OMP!t7Kj*h#9EZ%{ZpN2_IVxFc%LUpaQO1{sS zA+OiL{-&q1I&HXfMg))7{eJ$g6*yg2Z~m`0$=Jj_RjZf~HBe*n(`4~DVqfg8|MZq86XP^**q)0focKMA^(p(Fx zlXH}bKz*~TM|CKvY*G!+G6jR*#~1ErICiwGlvQ;^I%wu!BTRV$Sy$ptur9=wmKG0z zC^t{p&?B!yP+aC`LxbUDJT}clQ!a7+4wL?k0vNNJ$>8%|XlmINciA5c?iSiu$DZfG z2ic)#d4H#;D=H^U5#jvhTpf)-vKmFf!p%gSU%+!r`DBORL-z21f*$O{6*FbzFH5Bl8nT`6y#=+NgPXDE<*V zhE|-<)^pv~v0Q5xpF&2ITTA9PORAN#-_r+b=p!Dt6$N_sOBh-$jq{e{94Ie@a-mws zD#W`U24TG3yB-tRp!hbAAyW#x-LXDd|CNv-;3!7u6>tvJ{CT4PbEo;dB4CG%#5MF| ze0KWcG2|{d*?8c>u4j}D%(iV?p+YjE4Ehmvhg(-ri{v|;IP{kQA~HlHXb50u^0HU) zgtI+@X6K2j+7p)KZP{;YZY2 z^iLeJxtqFKCUA$Ug|8*s@eRToTg({$03c6a8KS?9SokA2HM1O5Xzp6aLMQ1n-g#>k zgqaO&3ggqo{z-%Y z#pYL*WprcSyXR!HzgnW<`gA`CCSZN4;?t2O9`iaG3p=O&dc>T`JQ}L;D|Mb**db-PH;SFTD z{#&c>Pq7;D7uOPlRyg4-N=zhpZdrk86T01tG0{O%xR@$VO3=*#{WVs~*gwYWGh`SuM8-8E?)*u}Bq1W8M?eL?a zInpJPc=SNzP%o_mD(w zqDoNl^yNgDaYpG#q24?WLVkJ~GDqL_SX<5ZqMA>0I67a6DEQ9s^+e5_@<&r<5y*1|CaYX~tHfrcY8N<~$5%G_b4 z(NFCP!9H3==X-q_p!Wx36K8zJwZ*`&xXQYm%akFR1xj@Ktgw&Ck>1Z7?CJ1ILta#z z^YMJ=Rhm)Q#=@=tEKr0&5gZ=8HO*Gv81vo0!`}4!up$^it{ISmV49VT`Cbt%n*?}ma#{Dl30F+J!5X8=l`EZYOf zff9yX#_nzs7VuO0%5a0c^P#HjjK1>Px>72%z{(sWd{Eac;_r# z*>S&bOotXmuL^{SXTw8rYF(|hr_>1gL*|3qatuQCDbvu&jU$mPH(NH}%Vzv1wJw~7Ix2+4Ff`6q z-`9+GiFu5-=7Xl;$5kf&KW5ThE(u{2yjsUaz38Cm(xodo`oA?0A|b(nOex#+lI^A- zMmfBh$#QX@XdTA=i@QenTnAK&U}84P!({RUu-{pD>PDfyq!e~PRC5<9u%U9UPd2tP zR}=`m`G?ZgIRFj{KKV5iu!}L*Gdjli`w@sK8wkG0ywFT8`F!wie!Bx-bX;z??f^7; zJX7iX4!xx24Z_AAc-{M;B2Qq|39*N!P-f{E1~Zy+Ih3L#mNDOS1<}OJ>5|{IUO;rfv+iC zX=ujMjn9L~o8%aO;Utt7E&x!u9QDw!fhW6j&d<+d;mX29-(5#4C}ys=-iM;b7Qa~l z8{GY!V`p~M8xkw>eUkC%njK{!O0&AwVa9Z(|86a10HOHl&GW;pwF_q3Vn5_1y=q$fbhXx{;o}4`2fETRsWI-xT0{IAX9jXFj z5Mg$}M}Ok*X=X^B_%o~gG8aXc2YX={*p^bIh8gFvjr^ZdVzjNdD*;Q&rYzGGE42uY z7}XX}*p{$+^*P`!nv>=nUU&HYH)1{VnO)T{{=B?*^}{=EczI*aubOy=90<>RDgF+;cK$?psDMG;HN`d~}hD4|;mv z^|a_h^G3X)yF1$xL%G@7tKAdOkkJT&t7;Sb^A+S)p?8HkgAQTpK+nr(4QOO!BpBY< zAregFGeZr>sxI8su$mWe5T^p(H17~yccP)a2ibZQO3nQt0yOu{T=fj0bdO}1n-8~0 z45ZhB$cAyJ4BA-(OfjI=tmT|AgcW8s16|#gc6gOf0RVJ1tI!e`8B|R4B0%Hr-!WV; zx-Zo%gS@nr{7ZVTklx;Nc3UpI&0W9bQM`znz3~nJ&Xz~t$T(aEbG&EB0SI#lC1B9@ z)AV6HmajnQ_U2BD`H)lH4lMhuFzVsRDB#Vkg&&#{Lu!s6_ScJZH*x$|zvHk8k@9}R z^_;8)_>^Qe5}PlUYxlTzTadG=X*(4K!L6+5=xXw(7~=qP1s` zOV8nakSx`(jttcZR-}2}6mg2}Y>+&Aat~!98~Myx8UFG>22U3Xl$$|%{_ zA87uW`K6f;iFK`33cOI=#SbNfc|C%EbaBM!y!}GOJN#v*KdJAzo#%XuprTgq?0Je= zfTQXD;iR*3E>Ak#XHKxIo}DfefhHvg{@sqir|G{G7SO0g>bBIS*E`m+Ohf_b zm4*qR5Q4Tar7YS$Z*(+-2JGVJS2(r-N;tJL{^Aj9KEZa=zrq4J1Aa}Re#sB%RZP9@ zuEVR8@w!8$Dy{f{i)<>_Tv+(`p}d^kK$*r~tV}0xXsPCs%7u9)&o{`yyPsnS z@76z|byS7r5J!Y@A;ABE4D$EG7=j4r5GSU32}`=EdrdMCScvcbwr#- z?Mo*&!o}MyOAWtYZH*Yj!;lvGRcW+nL6(=bsl9gf-yqPg8V2T!1!S7tH(s?7BHuf* zHfg3`N-0t@M(*<8z%#vnF<}t`W-E`Wi`Rkv z&|#Kw8ZGhhom^SD7S|Yh^3s%v2=d3|ft!L@vA4I+4=2X>uW7A*0_T4{-a|RyV*V&x zcrt#s$pnsI+8M~0jX7Dv$!^#MB8H{rx`}b`$P@)W)s3FWQ6XA;CP{y2as))~ezpg^ zQKb6nWFpaV=^;sFbGLaw_Ht^-ek&$NwDCa>W1_oAGU}&t$W@bEZ`CnWobOtGvd)q+ z*`bF1->Vf9OsG9%2dU%nd?e!?Jnl%fNs8X@s~F3n=Z*j`_XkxTS9??D=&!v(=Ec8I zNdPFwU2P{I$@lYBNtO|3X^jE)O%`M42R_#KYZYB9+s4Dg%vVSOBCkPVqHGea;bSWL z#yFK*eN{q42GXOeK{I+5-F!os?4qy0Sof^oxl#9EblZlBTg)y+AhlJ5$1OQ={tk5! z&V`#I>~n_}Hl@(#^AXOv3Bj$XqF&38F#-v5DF5fxTTi$;;V6Tic=ANbvJ*S#0(-o0 z;O+Wy=m&tzzqq+d>m`Mg;?H1Pfle)-aeXVOkR?%Pm4GPiBXu`P~D6|upVC%WwCh| zV>a6n^2HH-N822UaK&sSb$Omwa=5kLC?uyJN3=_J0)0}DrP-+3G&}en?<_gzB4n6- zTm?t!lGGbpTBnGGa^^v!W^Mm^Kb>OKHhH*upF`X$S&zDFD+Qc;@#%g`w-0FNmpKqB zW_5+@*Dr^QHCu+62z&S7G#2RU6BeP#LZx9{HN0Vs|-{xA5Zl%Gb>rmu&q zajaIG(HQJaVk^v!RHCZf7Z++8_@>;3PUP|AyRU~314#^(#PQ$pMj^~lth|09L8{rt zzF8$GvIn4y3*EV%iAfwc5BNJ5N#wdYSgWNgY^QBSNaSik&)d|+kh1x(RnFL0} z!8X6%$Y-VdDQ6!M9*la&jSgyf-&3hZ*yZ#YI03oJIstLcmwQuHaJ_alAq%;rZWbA1 zQ}$lCN~89oVhMcgW(`ENsNU4Pv4_QWr}v^YtUY7Ir>0i}i-QS+R4}1AM5l|p25wB^ zKg`D?tDY{&#%g2D43je0tSQAAodvAKW#{iX4$-o%^%p zT@1xiVp^IPZ4ofbDw{y7_3ccno`HlF`1o4><43V*%Ro9a7ab#G(S)ogO2W=SyBQMz zm#}N!GKtt^vPJ_VFp(B^bZMs1LygvN&08vmV|)U-1eps;$z`Be6Fn4@@A+FvF&oMd z?8xZN|2DY~6=7QW-MZyp!OM`!hmGi5W=;ICQ9@N*TkEWEYrB63bXf({s{>UF4`nm| zMnBf0K!&wO5=nn`YammuvnyOMI>wjn%}Y@sZC$+Vk?ihnE<7q{U5G z8AJZy?ey}W<*VxKzAl&QqM{s2a5Fy*n^eAWA&``m6b`2=4<%I4SA#QuNK1zgs$zmn zK7fXYhn>RwjC#zW=MAtW5hR8|GxoOi>2TCCtGiwc8ALuBzE+6s+jhtmnXcQJeK|^! zT_6cjMAm6nov2*0_}*C%MJe}z^daWOP}1-3MJgd1f}tb8(ALlmbO0G-ERIm-Zh#J911gjD-uo#j!hes`hJp zh$KAmX+?7Xj7|V{jPRj_-VvZwUDFk~u=%<;0pW0?Ge8oqVfipDBT8Ih%j<1WGterH zydg7xgi#nfL~|wIShtF?eKir6$Htj;eV|4im*d8Lwn;)2?F!ukiAXM3THKx8f<{g3 z+I=Sf9O#lGX6LZP&0OOkh*!+NQxG6IQC6gBVm65#s1~=lOCmuAbaQvBl7SKW{rUpS-Y$nAjU{*@jX$Np8{6p@pfPS7NjcJMqo1x4@DiwYt&sB{L+XPjU zC61ihri#CJD`MPG_Ci$Z*jPSSe?pq^)z|hO6VzKmte;4RoqdU~MbZy4*1&50H1o3x zZYI1;v9VE6gC>q;2y`yPr_7r=$bs9FxM*LH+5-l^bmJVmNlTm_BSNCI0*VH|9bNXT z1yMY8pZ@`I&NgO)1IxKpH)7NDo6Z@^*-bFJyrJ^kod(bL$(wU&$ESG3rUCfQV|1J4 zm1^bU@2ZmVM7T=*Z3CTN1N|Vl^XaMD=jK>_zYr+XuJW&if7i~vddC>?Q(ryzI#oqv#zCP=_VMq~YjKv5DWL2$(lIMGN>3c>#uY1a;0f?4~_5f_IzY z)W>iD&2mQbbbsOe21)2rUnD{@WOM1zmm(>z+N!YBFVgI#ILD}_eW2Yl=F&3PgBxJV zqNZ$E%n4kh

A^Ee=#OdCZp=gkO-Di`gaPo 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 MyARYWRIS7R2V#ULKL7v# literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e37ce368c..b2302a1c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,8 +6,12 @@ davx5app Account does not exist (anymore) - bitfire.at.davdroid - at.bitfire.davdroid.address_book + e.foundation.webdav + Google + e.foundation.webdav.google + eelo + e.foundation.webdav.eelo + foundation.e.accountmanager.address_book DAVx⁵ Address book at.bitfire.davdroid.addressbooks Address books diff --git a/app/src/main/res/xml/eelo_account_authenticator.xml b/app/src/main/res/xml/eelo_account_authenticator.xml new file mode 100644 index 000000000..90b164e22 --- /dev/null +++ b/app/src/main/res/xml/eelo_account_authenticator.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/xml/google_account_authenticator.xml b/app/src/main/res/xml/google_account_authenticator.xml new file mode 100644 index 000000000..0ba6c7a61 --- /dev/null +++ b/app/src/main/res/xml/google_account_authenticator.xml @@ -0,0 +1,16 @@ + + + + -- GitLab From 8a6c1652a7b892559c96e1e64991785174a593fc Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Thu, 7 Jun 2018 20:04:58 +0530 Subject: [PATCH 004/285] Auto-fill URL for the eelo Contacts and Calendar service. --- .../EeloAccountAuthenticatorService.kt | 1 + .../davdroid/ui/setup/LoginActivity.kt | 23 ++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt index f4b1a3c7f..26c7d9d3c 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt @@ -115,6 +115,7 @@ class EeloAccountAuthenticatorService : Service(), OnAccountsUpdateListener { ): Bundle { val intent = Intent(context, LoginActivity::class.java) intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response) + intent.putExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, LoginActivity.ACCOUNT_PROVIDER_EELO) val bundle = Bundle(1) bundle.putParcelable(AccountManager.KEY_INTENT, intent) return bundle diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt index e03c39899..35758c735 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt @@ -42,6 +42,9 @@ class LoginActivity: AppCompatActivity() { */ const val EXTRA_PASSWORD = "password" + const val SETUP_ACCOUNT_PROVIDER_TYPE = "setup_account_provider_type" + const val ACCOUNT_PROVIDER_EELO = "eelo" + const val ACCOUNT_PROVIDER_GOOGLE = "google" } @Inject @@ -63,9 +66,23 @@ class LoginActivity: AppCompatActivity() { } if (fragment != null) { - supportFragmentManager.beginTransaction() - .replace(android.R.id.content, fragment) - .commit() + when (intent.getStringExtra(SETUP_ACCOUNT_PROVIDER_TYPE)) { + ACCOUNT_PROVIDER_EELO -> { + intent.putExtra(EXTRA_URL, "https://drive.eelo.io") + // first call, add first login fragment + supportFragmentManager.beginTransaction() + .replace(android.R.id.content, fragment) + .commit() + } + ACCOUNT_PROVIDER_GOOGLE -> { + print("starting google account sign-in") + } + else -> + // first call, add first login fragment + supportFragmentManager.beginTransaction() + .replace(android.R.id.content, fragment) + .commit() + } } else Logger.log.severe("Couldn't create LoginFragment") } -- GitLab From 9baeaef3f06b5912fece626c002be0c50ec28b6f Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Thu, 7 Jun 2018 23:12:08 +0530 Subject: [PATCH 005/285] Implemented Google authentication. --- app/build.gradle | 8 + app/src/main/AndroidManifest.xml | 39 +++ .../authorization/IdentityProvider.java | 236 ++++++++++++++++++ .../GoogleAccountAuthenticatorService.kt | 1 + .../ui/setup/GoogleAuthenticatorFragment.kt | 172 +++++++++++++ .../davdroid/ui/setup/LoginActivity.kt | 6 +- .../layout/fragment_google_authenticator.xml | 23 ++ .../values/email_providers_auth_config.xml | 18 ++ gradle.properties | 1 + 9 files changed, 503 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/at/bitfire/davdroid/authorization/IdentityProvider.java create mode 100644 app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt create mode 100644 app/src/main/res/layout/fragment_google_authenticator.xml create mode 100644 app/src/main/res/values/email_providers_auth_config.xml diff --git a/app/build.gradle b/app/build.gradle index 012ff381e..7b272ad90 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,6 +6,7 @@ apply plugin: 'com.android.application' apply plugin: 'com.mikepenz.aboutlibraries.plugin' apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { @@ -93,6 +94,12 @@ android { excludes += ['META-INF/*.md'] } } + + defaultConfig { + manifestPlaceholders = [ + 'appAuthRedirectScheme': 'com.googleusercontent.apps.628867657910-7ade6gut5rhabdgjq6k4rln9i1u9ppca' + ] + } } dependencies { @@ -147,6 +154,7 @@ dependencies { implementation "org.apache.commons:commons-lang3:${versions.commonsLang}" //noinspection GradleDependency implementation "org.apache.commons:commons-text:${versions.commonsText}" + implementation 'net.openid:appauth:0.11.1' // for tests androidTestImplementation "com.google.dagger:hilt-android-testing:${versions.hilt}" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5c69b7e8e..b89beb982 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -286,6 +286,45 @@ android:name="android.accounts.AccountAuthenticator" android:resource="@xml/eelo_account_authenticator" /> + + + + + + + + + + + + + + + + + + + + + + + + + + . + */ + +package at.bitfire.davdroid.authorization; + +import android.content.Context; +import android.content.res.Resources; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import net.openid.appauth.AuthorizationServiceConfiguration; +import net.openid.appauth.AuthorizationServiceConfiguration.RetrieveConfigurationCallback; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import at.bitfire.davdroid.R; + +/** + * An abstraction of identity providers, containing all necessary info for the demo app. + */ +public class IdentityProvider { + + /** + * Value used to indicate that a configured property is not specified or required. + */ + public static final int NOT_SPECIFIED = -1; + + public static final IdentityProvider GOOGLE = new IdentityProvider( + "Google", + R.string.google_discovery_uri, + NOT_SPECIFIED, // auth endpoint is discovered + NOT_SPECIFIED, // token endpoint is discovered + R.string.google_client_id, + NOT_SPECIFIED, // client secret is not required for Google + R.string.google_auth_redirect_uri, + R.string.google_scope_string, + R.string.google_name); + + public static final List PROVIDERS = Arrays.asList( + GOOGLE); + + public static List getEnabledProviders(Context context) { + ArrayList providers = new ArrayList<>(); + for (IdentityProvider provider : PROVIDERS) { + provider.readConfiguration(context); + providers.add(provider); + } + return providers; + } + + @NonNull + public final String name; + + @StringRes + public final int buttonContentDescriptionRes; + + @StringRes + private final int mDiscoveryEndpointRes; + + @StringRes + private final int mAuthEndpointRes; + + @StringRes + private final int mTokenEndpointRes; + + @StringRes + private final int mClientIdRes; + + @StringRes + private final int mClientSecretRes; + + @StringRes + private final int mRedirectUriRes; + + @StringRes + private final int mScopeRes; + + private boolean mConfigurationRead = false; + private Uri mDiscoveryEndpoint; + private Uri mAuthEndpoint; + private Uri mTokenEndpoint; + private String mClientId; + private String mClientSecret; + private Uri mRedirectUri; + private String mScope; + + IdentityProvider( + @NonNull String name, + @StringRes int discoveryEndpointRes, + @StringRes int authEndpointRes, + @StringRes int tokenEndpointRes, + @StringRes int clientIdRes, + @StringRes int clientSecretRes, + @StringRes int redirectUriRes, + @StringRes int scopeRes, + @StringRes int buttonContentDescriptionRes) { + if (!isSpecified(discoveryEndpointRes) + && !isSpecified(authEndpointRes) + && !isSpecified(tokenEndpointRes)) { + throw new IllegalArgumentException( + "the discovery endpoint or the auth and token endpoints must be specified"); + } + + this.name = name; + this.mDiscoveryEndpointRes = discoveryEndpointRes; + this.mAuthEndpointRes = authEndpointRes; + this.mTokenEndpointRes = tokenEndpointRes; + this.mClientIdRes = checkSpecified(clientIdRes, "clientIdRes"); + this.mClientSecretRes = clientSecretRes; + this.mRedirectUriRes = checkSpecified(redirectUriRes, "redirectUriRes"); + this.mScopeRes = checkSpecified(scopeRes, "scopeRes"); + this.buttonContentDescriptionRes = + checkSpecified(buttonContentDescriptionRes, "buttonContentDescriptionRes"); + } + + /** + * This must be called before any of the getters will function. + */ + public void readConfiguration(Context context) { + if (mConfigurationRead) { + return; + } + + Resources res = context.getResources(); + + mDiscoveryEndpoint = isSpecified(mDiscoveryEndpointRes) + ? getUriResource(res, mDiscoveryEndpointRes, "discoveryEndpointRes") + : null; + mAuthEndpoint = isSpecified(mAuthEndpointRes) + ? getUriResource(res, mAuthEndpointRes, "authEndpointRes") + : null; + mTokenEndpoint = isSpecified(mTokenEndpointRes) + ? getUriResource(res, mTokenEndpointRes, "tokenEndpointRes") + : null; + mClientId = res.getString(mClientIdRes); + mClientSecret = isSpecified(mClientSecretRes) ? res.getString(mClientSecretRes) : null; + mRedirectUri = getUriResource(res, mRedirectUriRes, "mRedirectUriRes"); + mScope = res.getString(mScopeRes); + + mConfigurationRead = true; + } + + private void checkConfigurationRead() { + if (!mConfigurationRead) { + throw new IllegalStateException("Configuration not read"); + } + } + + @Nullable + public Uri getDiscoveryEndpoint() { + checkConfigurationRead(); + return mDiscoveryEndpoint; + } + + @Nullable + public Uri getAuthEndpoint() { + checkConfigurationRead(); + return mAuthEndpoint; + } + + @Nullable + public Uri getTokenEndpoint() { + checkConfigurationRead(); + return mTokenEndpoint; + } + + @NonNull + public String getClientId() { + checkConfigurationRead(); + return mClientId; + } + + @Nullable + public String getClientSecret() { + checkConfigurationRead(); + return mClientSecret; + } + + @NonNull + public Uri getRedirectUri() { + checkConfigurationRead(); + return mRedirectUri; + } + + @NonNull + public String getScope() { + checkConfigurationRead(); + return mScope; + } + + public void retrieveConfig(Context context, + RetrieveConfigurationCallback callback) { + readConfiguration(context); + if (getDiscoveryEndpoint() != null) { + AuthorizationServiceConfiguration.fetchFromUrl(mDiscoveryEndpoint, callback); + } else { + AuthorizationServiceConfiguration config = + new AuthorizationServiceConfiguration(mAuthEndpoint, mTokenEndpoint, null); + callback.onFetchConfigurationCompleted(config, null); + } + } + + private static boolean isSpecified(int value) { + return value != NOT_SPECIFIED; + } + + private static int checkSpecified(int value, String valueName) { + if (value == NOT_SPECIFIED) { + throw new IllegalArgumentException(valueName + " must be specified"); + } + return value; + } + + private static Uri getUriResource(Resources res, @StringRes int resId, String resName) { + return Uri.parse(res.getString(resId)); + } +} + diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt index b25532ad1..7a8c35c37 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt @@ -114,6 +114,7 @@ class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener { ): Bundle { val intent = Intent(context, LoginActivity::class.java) intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response) + intent.putExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, LoginActivity.ACCOUNT_PROVIDER_GOOGLE) val bundle = Bundle(1) bundle.putParcelable(AccountManager.KEY_INTENT, intent) return bundle diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt new file mode 100644 index 000000000..cb1c9dacf --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt @@ -0,0 +1,172 @@ +/* + * Copyright ECORP SAS 2022 + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 at.bitfire.davdroid.ui.setup + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import at.bitfire.davdroid.R +import at.bitfire.davdroid.authorization.IdentityProvider +import kotlinx.android.synthetic.main.fragment_google_authenticator.* +import net.openid.appauth.* + + +class GoogleAuthenticatorFragment : Fragment() { + + private val EXTRA_AUTH_SERVICE_DISCOVERY = "authServiceDiscovery" + private val EXTRA_CLIENT_SECRET = "clientSecret" + + private var authState: AuthState? = null + private var authorizationService: AuthorizationService? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.fragment_google_authenticator, container, false) + + // Initialise the authorization service + authorizationService = AuthorizationService(context!!) + + activity?.intent?.let { + if (!with(it) { getBooleanExtra(LoginActivity.ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE, false) }) { + // Get all the account providers + val providers = IdentityProvider.getEnabledProviders(context) + + // Iterate over the account providers + for (idp in providers) { + val retrieveCallback = AuthorizationServiceConfiguration.RetrieveConfigurationCallback { serviceConfiguration, ex -> + if (ex == null && serviceConfiguration != null) { + makeAuthRequest(serviceConfiguration, idp) + } + else { + // TODO Handle error + } + } + + if (idp.name.equals(getString(R.string.google_name))) { + // Get configurations for the Google account provider + idp.retrieveConfig(context, retrieveCallback) + } + } + } + else { + if (authState == null) { + val response = AuthorizationResponse.fromIntent(activity!!.intent) + val ex = AuthorizationException.fromIntent(activity!!.intent) + authState = AuthState(response, ex) + + if (response != null) { + exchangeAuthorizationCode(response) + } + else { + // TODO Handle error + } + } + } + } + + return view + } + + private fun makeAuthRequest( + serviceConfig: AuthorizationServiceConfiguration, + idp: IdentityProvider) { + + val authRequest = AuthorizationRequest.Builder( + serviceConfig, + idp.clientId, + ResponseTypeValues.CODE, + idp.redirectUri) + .setScope(idp.scope) + .build() + + authorizationService?.performAuthorizationRequest( + authRequest, + createPostAuthorizationIntent( + context!!, + authRequest, + serviceConfig.discoveryDoc, + idp.clientSecret), + authorizationService?.createCustomTabsIntentBuilder()!! + .build()) + + activity?.finish() + } + + private fun createPostAuthorizationIntent( + context: Context, + request: AuthorizationRequest, + discoveryDoc: AuthorizationServiceDiscovery?, + clientSecret: String?): PendingIntent { + val intent = Intent(context, LoginActivity::class.java) + + if (discoveryDoc != null) { + intent.putExtra(EXTRA_AUTH_SERVICE_DISCOVERY, discoveryDoc.docJson.toString()) + } + + if (clientSecret != null) { + intent.putExtra(EXTRA_CLIENT_SECRET, clientSecret) + } + + intent.putExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, LoginActivity.ACCOUNT_PROVIDER_GOOGLE) + intent.putExtra(LoginActivity.ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE, true) + + return PendingIntent.getActivity(context, request.hashCode(), intent, 0) + } + + private fun exchangeAuthorizationCode(authorizationResponse: AuthorizationResponse) { + val additionalParams = HashMap() + if (getClientSecretFromIntent(activity!!.intent) != null) { + additionalParams["client_secret"] = getClientSecretFromIntent(activity!!.intent) + } + performTokenRequest(authorizationResponse.createTokenExchangeRequest(additionalParams)) + } + + private fun getClientSecretFromIntent(intent: Intent): String? { + return if (!intent.hasExtra(EXTRA_CLIENT_SECRET)) { + null + } + else intent.getStringExtra(EXTRA_CLIENT_SECRET) + } + + + private fun performTokenRequest(request: TokenRequest) { + authorizationService?.performTokenRequest( + request, + AuthorizationService.TokenResponseCallback { tokenResponse, ex -> receivedTokenResponse(tokenResponse, ex) }) + } + + private fun receivedTokenResponse( + tokenResponse: TokenResponse?, + authException: AuthorizationException?) { + authState?.update(tokenResponse, authException) + progress_bar.visibility = View.GONE + auth_token_success_text_view.visibility = View.VISIBLE + } + + override fun onDestroy() { + super.onDestroy() + authorizationService?.dispose() + } + + +} + diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt index 35758c735..f963eb96a 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt @@ -45,6 +45,7 @@ class LoginActivity: AppCompatActivity() { const val SETUP_ACCOUNT_PROVIDER_TYPE = "setup_account_provider_type" const val ACCOUNT_PROVIDER_EELO = "eelo" const val ACCOUNT_PROVIDER_GOOGLE = "google" + const val ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE = "google_auth_complete" } @Inject @@ -68,6 +69,7 @@ class LoginActivity: AppCompatActivity() { if (fragment != null) { when (intent.getStringExtra(SETUP_ACCOUNT_PROVIDER_TYPE)) { ACCOUNT_PROVIDER_EELO -> { + // Set the eelo Contacts and Calendar service URL intent.putExtra(EXTRA_URL, "https://drive.eelo.io") // first call, add first login fragment supportFragmentManager.beginTransaction() @@ -75,7 +77,9 @@ class LoginActivity: AppCompatActivity() { .commit() } ACCOUNT_PROVIDER_GOOGLE -> { - print("starting google account sign-in") + supportFragmentManager.beginTransaction() + .replace(android.R.id.content, GoogleAuthenticatorFragment()) + .commit() } else -> // first call, add first login fragment diff --git a/app/src/main/res/layout/fragment_google_authenticator.xml b/app/src/main/res/layout/fragment_google_authenticator.xml new file mode 100644 index 000000000..eca69e0bb --- /dev/null +++ b/app/src/main/res/layout/fragment_google_authenticator.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/app/src/main/res/values/email_providers_auth_config.xml b/app/src/main/res/values/email_providers_auth_config.xml new file mode 100644 index 000000000..597bf0f9b --- /dev/null +++ b/app/src/main/res/values/email_providers_auth_config.xml @@ -0,0 +1,18 @@ + + + + + Google + + 100496780587-pbiu5eudcjm6cge2phduc6mt8mgbsmsr.apps.googleusercontent.com + + https://accounts.google.com/.well-known/openid-configuration + + openid profile email https://www.googleapis.com/auth/carddav + + + com.googleusercontent.apps.100496780587-pbiu5eudcjm6cge2phduc6mt8mgbsmsr:/oauth2redirect + + + + diff --git a/gradle.properties b/gradle.properties index c6bf06227..824f2fa91 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,3 +16,4 @@ android.useAndroidX=true org.gradle.daemon=true org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1536M" org.gradle.parallel=true +android.enableJetifier=true -- GitLab From 8432029dc574a9293927d7744a752732cdf968ef Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Fri, 8 Jun 2018 11:04:15 +0530 Subject: [PATCH 006/285] Updated Kotlin plugin version. Get account details upon successful authentication/authorization. Added Google Calendar scope for authorization. --- .../ui/setup/GoogleAuthenticatorFragment.kt | 179 ++++++++++++++++-- .../values/email_providers_auth_config.xml | 2 +- 2 files changed, 166 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt index cb1c9dacf..5a6561f0a 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt @@ -19,7 +19,10 @@ package at.bitfire.davdroid.ui.setup import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.os.AsyncTask import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -28,16 +31,28 @@ import at.bitfire.davdroid.R import at.bitfire.davdroid.authorization.IdentityProvider import kotlinx.android.synthetic.main.fragment_google_authenticator.* import net.openid.appauth.* +import org.json.JSONException +import org.json.JSONObject +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.net.HttpURLConnection +import java.net.MalformedURLException +import java.net.URL -class GoogleAuthenticatorFragment : Fragment() { +class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenResponseCallback { - private val EXTRA_AUTH_SERVICE_DISCOVERY = "authServiceDiscovery" - private val EXTRA_CLIENT_SECRET = "clientSecret" + private val extraAuthServiceDiscovery = "authServiceDiscovery" + private val extraClientSecret = "clientSecret" private var authState: AuthState? = null private var authorizationService: AuthorizationService? = null + private val bufferSize = 1024 + private var userInfoJson: JSONObject? = null + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_google_authenticator, container, false) @@ -61,7 +76,7 @@ class GoogleAuthenticatorFragment : Fragment() { } } - if (idp.name.equals(getString(R.string.google_name))) { + if (idp.name == getString(R.string.google_name)) { // Get configurations for the Google account provider idp.retrieveConfig(context, retrieveCallback) } @@ -119,11 +134,11 @@ class GoogleAuthenticatorFragment : Fragment() { val intent = Intent(context, LoginActivity::class.java) if (discoveryDoc != null) { - intent.putExtra(EXTRA_AUTH_SERVICE_DISCOVERY, discoveryDoc.docJson.toString()) + intent.putExtra(extraAuthServiceDiscovery, discoveryDoc.docJson.toString()) } if (clientSecret != null) { - intent.putExtra(EXTRA_CLIENT_SECRET, clientSecret) + intent.putExtra(extraClientSecret, clientSecret) } intent.putExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, LoginActivity.ACCOUNT_PROVIDER_GOOGLE) @@ -141,25 +156,161 @@ class GoogleAuthenticatorFragment : Fragment() { } private fun getClientSecretFromIntent(intent: Intent): String? { - return if (!intent.hasExtra(EXTRA_CLIENT_SECRET)) { + return if (!intent.hasExtra(extraClientSecret)) { null } - else intent.getStringExtra(EXTRA_CLIENT_SECRET) + else intent.getStringExtra(extraClientSecret) } private fun performTokenRequest(request: TokenRequest) { authorizationService?.performTokenRequest( - request, - AuthorizationService.TokenResponseCallback { tokenResponse, ex -> receivedTokenResponse(tokenResponse, ex) }) + request, this) } - private fun receivedTokenResponse( - tokenResponse: TokenResponse?, - authException: AuthorizationException?) { - authState?.update(tokenResponse, authException) + override fun onTokenRequestCompleted(response: TokenResponse?, ex: AuthorizationException?) { + authState?.update(response, ex) progress_bar.visibility = View.GONE auth_token_success_text_view.visibility = View.VISIBLE + + getAccountInfo() + } + + private fun getAccountInfo() { + val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent) + + if (!authState!!.isAuthorized + || discoveryDoc == null + || discoveryDoc.userinfoEndpoint == null) { + //TODO Error occurred + } + else { + object : AsyncTask() { + override fun doInBackground(vararg params: Void): Void? { + fetchUserInfo() + return null + } + }.execute() + } + } + + private fun getDiscoveryDocFromIntent(intent: Intent): AuthorizationServiceDiscovery? { + if (!intent.hasExtra(extraAuthServiceDiscovery)) { + return null + } + val discoveryJson = intent.getStringExtra(extraAuthServiceDiscovery) + try { + return AuthorizationServiceDiscovery(JSONObject(discoveryJson)) + } + catch (ex: JSONException) { + throw IllegalStateException("Malformed JSON in discovery doc") + } + catch (ex: AuthorizationServiceDiscovery.MissingArgumentException) { + throw IllegalStateException("Malformed JSON in discovery doc") + } + + } + + private fun fetchUserInfo() { + if (authState!!.authorizationServiceConfiguration == null) { + // TODO Handle error due to unavailable service configuration + return + } + + authState!!.performActionWithFreshTokens(authorizationService!!, AuthState.AuthStateAction { accessToken, _, ex -> + if (ex != null) { + // TODO An exception occurred, handle error + return@AuthStateAction + } + + val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent) + ?: throw IllegalStateException("no available discovery doc") + + val userInfoEndpoint: URL + try { + userInfoEndpoint = URL(discoveryDoc.userinfoEndpoint!!.toString()) + } + catch (urlEx: MalformedURLException) { + // TODO Handle error due to malformed URL + return@AuthStateAction + } + + var userInfoResponse: InputStream? = null + try { + val conn = userInfoEndpoint.openConnection() as HttpURLConnection + conn.setRequestProperty("Authorization", "Bearer " + accessToken!!) + conn.instanceFollowRedirects = false + userInfoResponse = conn.inputStream + val response = readStream(userInfoResponse) + updateUserInfo(JSONObject(response)) + } + catch (ioEx: IOException) { + // TODO Handle network error + } + catch (jsonEx: JSONException) { + // TODO Handle JSON parse error + } + finally { + if (userInfoResponse != null) { + try { + userInfoResponse.close() + } + catch (ioEx: IOException) { + // TODO Handle network exception while closing response stream + } + + } + } + }) + } + + @Throws(IOException::class) + private fun readStream(stream: InputStream?): String { + val br = BufferedReader(InputStreamReader(stream!!)) + val buffer = CharArray(bufferSize) + val sb = StringBuilder() + var readCount = br.read(buffer) + while (readCount != -1) { + sb.append(buffer, 0, readCount) + readCount = br.read(buffer) + } + return sb.toString() + } + + private fun updateUserInfo(jsonObject: JSONObject) { + Handler(Looper.getMainLooper()).post { + userInfoJson = jsonObject + onAccountInfoGotten() + } + } + + private fun onAccountInfoGotten() { + if (userInfoJson != null) { + try { + var name = "Unknown" + if (userInfoJson!!.has("name")) { + name = userInfoJson!!.getString("name") + } + + var emailAddress: String? = null + if (userInfoJson!!.has("email")) { + emailAddress = userInfoJson!!.getString("email") + } + + /*account.setName(name) + account.setEmailAddress(emailAddress) + account.setAuthToken(authState!!.accessToken) + account.setRefreshToken(authState!!.refreshToken)*/ + } + catch (ex: JSONException) { + // TODO Handle JSON parse error + } + + } + else { + //TODO Handle error + } + } override fun onDestroy() { diff --git a/app/src/main/res/values/email_providers_auth_config.xml b/app/src/main/res/values/email_providers_auth_config.xml index 597bf0f9b..f4f123158 100644 --- a/app/src/main/res/values/email_providers_auth_config.xml +++ b/app/src/main/res/values/email_providers_auth_config.xml @@ -8,7 +8,7 @@ https://accounts.google.com/.well-known/openid-configuration - openid profile email https://www.googleapis.com/auth/carddav + openid profile email https://www.googleapis.com/auth/carddav https://www.googleapis.com/auth/calendar com.googleusercontent.apps.100496780587-pbiu5eudcjm6cge2phduc6mt8mgbsmsr:/oauth2redirect -- GitLab From e5604118fd370db56f1b40fffd6974f3f044c2ce Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Fri, 19 Aug 2022 12:16:09 +0200 Subject: [PATCH 007/285] confirmation dialog for removing webdav mount (closes bitfireAT/davx5#47) (#119) * confirmation dialog for removing webdav mount Co-authored-by: Ricki Hirner --- .../ui/webdav/WebdavMountsActivity.kt | 21 ++++++++++++++++--- app/src/main/res/drawable/ic_eject.xml | 5 ----- app/src/main/res/drawable/ic_remove.xml | 5 +++++ .../main/res/layout/webdav_mounts_item.xml | 4 ++-- app/src/main/res/values/strings.xml | 4 ++++ 5 files changed, 29 insertions(+), 10 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_eject.xml create mode 100644 app/src/main/res/drawable/ic_remove.xml diff --git a/app/src/main/java/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt index 149d76b94..3fdd02509 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt @@ -4,6 +4,7 @@ package at.bitfire.davdroid.ui.webdav +import android.app.AlertDialog import android.content.Context import android.content.Intent import android.os.Bundle @@ -26,6 +27,7 @@ import at.bitfire.davdroid.databinding.WebdavMountsItemBinding import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.WebDavDocument import at.bitfire.davdroid.db.WebDavMount +import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.ui.UiUtils import at.bitfire.davdroid.webdav.CredentialsStore import at.bitfire.davdroid.webdav.DavDocumentsProvider @@ -35,8 +37,10 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.apache.commons.io.FileUtils +import java.util.logging.Level import javax.inject.Inject + @AndroidEntryPoint class WebdavMountsActivity: AppCompatActivity() { @@ -162,8 +166,16 @@ class WebdavMountsActivity: AppCompatActivity() { model.browseIntent.value = null } - binding.unmount.setOnClickListener { - model.unmount(info.mount) + binding.removeMountpoint.setOnClickListener { + AlertDialog.Builder(context) + .setTitle(R.string.webdav_remove_mount_title) + .setMessage(R.string.webdav_remove_mount_text) + .setPositiveButton(R.string.dialog_remove) { _, _ -> + Logger.log.log(Level.INFO, "User removes mount point", info.mount) + model.remove(info.mount) + } + .setNegativeButton(R.string.dialog_deny, null) + .show() } } } @@ -212,7 +224,10 @@ class WebdavMountsActivity: AppCompatActivity() { val browseIntent = MutableLiveData() - fun unmount(mount: WebDavMount) { + /** + * Removes the mountpoint (deleting connection information) + */ + fun remove(mount: WebDavMount) { viewModelScope.launch(Dispatchers.IO) { // remove mount from database db.webDavMountDao().delete(mount) diff --git a/app/src/main/res/drawable/ic_eject.xml b/app/src/main/res/drawable/ic_eject.xml deleted file mode 100644 index 0a8c42659..000000000 --- a/app/src/main/res/drawable/ic_eject.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_remove.xml b/app/src/main/res/drawable/ic_remove.xml new file mode 100644 index 000000000..8561a3390 --- /dev/null +++ b/app/src/main/res/drawable/ic_remove.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/webdav_mounts_item.xml b/app/src/main/res/layout/webdav_mounts_item.xml index 3f66c9e33..627b2105d 100644 --- a/app/src/main/res/layout/webdav_mounts_item.xml +++ b/app/src/main/res/layout/webdav_mounts_item.xml @@ -69,13 +69,13 @@ android:text="@string/webdav_mounts_share_content" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e37ce368c..e1efc0bac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,8 @@ DAVx⁵ Address book at.bitfire.davdroid.addressbooks Address books + Remove + Cancel This field is required Help Manage accounts @@ -462,6 +464,8 @@ Password Add mount No WebDAV service at this URL + Remove mount point + Connection details will be lost, but no files will be deleted. Accessing WebDAV file Downloading WebDAV file Uploading WebDAV file -- GitLab From 885035fadfc5f6feb707b5c72279e9f4d092e0b7 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 15 Aug 2022 12:39:57 +0200 Subject: [PATCH 008/285] Update dependencies --- app/build.gradle | 4 ++-- build.gradle | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b0970d16e..4308cfb06 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -107,12 +107,12 @@ dependencies { implementation "com.google.dagger:hilt-android:${versions.hilt}" kapt "com.google.dagger:hilt-android-compiler:${versions.hilt}" - implementation 'androidx.appcompat:appcompat:1.4.2' + implementation 'androidx.appcompat:appcompat:1.5.0' implementation 'androidx.browser:browser:1.4.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.core:core-ktx:1.8.0' - implementation 'androidx.fragment:fragment-ktx:1.5.1' + implementation 'androidx.fragment:fragment-ktx:1.5.2' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' implementation 'androidx.paging:paging-runtime-ktx:3.1.1' diff --git a/build.gradle b/build.gradle index d06901aa3..fb131ffaa 100644 --- a/build.gradle +++ b/build.gradle @@ -9,11 +9,11 @@ buildscript { ext.versions = [ aboutLibraries: '8.9.4', - appIntro: '6.1.0', + appIntro: '6.2.0', dav4jvm: 'c61e4b0c80a5a8de1df99b4997445bb323d3ea3d', hilt: '2.42', kotlin: '1.7.0', - okhttp: '4.9.3', + okhttp: '4.10.0', // latest Apache Commons versions that don't require Java 8 (Android 7) commonsCollections: '4.2', commonsLang: '3.8.1', -- GitLab From fec8eae9d0e57cc75860dd9190b561a2fdb5e233 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 19 Aug 2022 12:33:36 +0200 Subject: [PATCH 009/285] Update dependencies, including ical4android --- ical4android | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ical4android b/ical4android index 41171b57f..ccea6cbd4 160000 --- a/ical4android +++ b/ical4android @@ -1 +1 @@ -Subproject commit 41171b57fdc5f045b17d09df85853a6cc13ef4fd +Subproject commit ccea6cbd487b0872d7e399f290fd00ffbb08f37f -- GitLab From 761ac44e1fcf130f4b0746c07660af7185299794 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Fri, 19 Aug 2022 19:29:51 +0600 Subject: [PATCH 010/285] add dav4android submodule --- .gitmodules | 4 ++++ app/build.gradle | 4 +--- dav4android | 1 + settings.gradle | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) create mode 160000 dav4android diff --git a/.gitmodules b/.gitmodules index 6a6b94f2b..647569050 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,7 @@ [submodule "cert4android"] path = cert4android url = https://github.com/bitfireAT/cert4android.git +[submodule "dav4android"] + path = dav4android + url = https://gitlab.e.foundation/e/apps/dav4android.git + branch = main diff --git a/app/build.gradle b/app/build.gradle index 7b272ad90..bf45cbef8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -106,6 +106,7 @@ dependencies { implementation project(':cert4android') implementation project(':ical4android') implementation project(':vcard4android') + implementation project(':dav4android') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0" @@ -138,9 +139,6 @@ dependencies { implementation 'com.jaredrummler:colorpicker:1.1.0' implementation "com.github.AppIntro:AppIntro:${versions.appIntro}" - implementation("com.github.bitfireAT:dav4jvm:${versions.dav4jvm}") { - exclude group: 'junit' - } implementation "com.mikepenz:aboutlibraries:${versions.aboutLibraries}" implementation "com.squareup.okhttp3:okhttp:${versions.okhttp}" implementation "com.squareup.okhttp3:okhttp-brotli:${versions.okhttp}" diff --git a/dav4android b/dav4android new file mode 160000 index 000000000..05b97db35 --- /dev/null +++ b/dav4android @@ -0,0 +1 @@ +Subproject commit 05b97db35f73185cb8fefc708e882cbf0b555ed1 diff --git a/settings.gradle b/settings.gradle index e5c3923c5..e9ca5a41c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,3 +3,4 @@ include ':app' include ':cert4android' include ':ical4android' include ':vcard4android' +include ':dav4android' \ No newline at end of file -- GitLab From 714a63f2ed6343bf272a84d277f2481413345580 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Fri, 19 Aug 2022 20:32:22 +0600 Subject: [PATCH 011/285] Implemented Google Account setup --- .../at/bitfire/davdroid/db/Credentials.kt | 2 + .../davdroid/settings/AccountSettings.kt | 4 ++ .../syncadapter/CalendarSyncManager.kt | 2 +- .../syncadapter/ContactsSyncManager.kt | 2 +- .../davdroid/syncadapter/JtxSyncManager.kt | 2 +- .../davdroid/syncadapter/TasksSyncManager.kt | 2 +- .../davdroid/ui/setup/DavResourceFinder.kt | 8 +-- .../ui/setup/GoogleAuthenticatorFragment.kt | 66 ++++++++++++++++--- .../ui/setup/GoogleAuthenticatorModel.kt | 48 ++++++++++++++ .../davdroid/webdav/DavDocumentsProvider.kt | 2 +- .../layout/fragment_google_authenticator.xml | 44 ++++++++----- app/src/main/res/values/strings.xml | 6 +- 12 files changed, 151 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorModel.kt diff --git a/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt b/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt index f2f4758b1..cc80bf405 100644 --- a/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt +++ b/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt @@ -7,6 +7,8 @@ package at.bitfire.davdroid.db data class Credentials( val userName: String? = null, val password: String? = null, + val accessToken: String? = null, + val refreshToken: String? = null, val certificateAlias: String? = null ) { diff --git a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt index 8d414c121..13d3ee569 100644 --- a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt @@ -89,6 +89,8 @@ class AccountSettings( const val KEY_SYNC_INTERVAL_TASKS = "sync_interval_tasks" const val KEY_USERNAME = "user_name" + const val KEY_ACCESS_TOKEN = "access_token" + const val KEY_REFRESH_TOKEN = "refresh_token" const val KEY_CERTIFICATE_ALIAS = "certificate_alias" const val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false) @@ -232,6 +234,8 @@ class AccountSettings( fun credentials() = Credentials( accountManager.getUserData(account, KEY_USERNAME), accountManager.getPassword(account), + accountManager.getUserData(account, KEY_ACCESS_TOKEN), + accountManager.getUserData(account, KEY_REFRESH_TOKEN), accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS) ) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarSyncManager.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarSyncManager.kt index c38400551..8540d3034 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarSyncManager.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarSyncManager.kt @@ -55,7 +55,7 @@ class CalendarSyncManager( override fun prepare(): Boolean { collectionURL = (localCollection.name ?: return false).toHttpUrlOrNull() ?: return false - davCollection = DavCalendar(httpClient.okHttpClient, collectionURL) + davCollection = DavCalendar(httpClient.okHttpClient, collectionURL, accountSettings.credentials().accessToken) // if there are dirty exceptions for events, mark their master events as dirty, too localCollection.processDirtyExceptions() diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt index 2401a0a0d..d9d901bdb 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt @@ -121,7 +121,7 @@ class ContactsSyncManager( } collectionURL = localCollection.url.toHttpUrlOrNull() ?: return false - davCollection = DavAddressBook(httpClient.okHttpClient, collectionURL) + davCollection = DavAddressBook(httpClient.okHttpClient, collectionURL, accountSettings.credentials().accessToken) resourceDownloader = ResourceDownloader(davCollection.location) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncManager.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncManager.kt index 5ecdc496a..a644d35ff 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncManager.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncManager.kt @@ -47,7 +47,7 @@ class JtxSyncManager( override fun prepare(): Boolean { collectionURL = (localCollection.url ?: return false).toHttpUrlOrNull() ?: return false - davCollection = DavCalendar(httpClient.okHttpClient, collectionURL) + davCollection = DavCalendar(httpClient.okHttpClient, collectionURL, accountSettings.credentials().accessToken) return true } diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.kt index d1dbc363e..68ad8d06a 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.kt @@ -50,7 +50,7 @@ class TasksSyncManager( override fun prepare(): Boolean { collectionURL = (localCollection.syncId ?: return false).toHttpUrlOrNull() ?: return false - davCollection = DavCalendar(httpClient.okHttpClient, collectionURL) + davCollection = DavCalendar(httpClient.okHttpClient, collectionURL, accountSettings.credentials().accessToken) return true } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/DavResourceFinder.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/DavResourceFinder.kt index f4f4b7bc0..cd346e432 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/DavResourceFinder.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/DavResourceFinder.kt @@ -163,7 +163,7 @@ class DavResourceFinder( private fun checkUserGivenURL(baseURL: HttpUrl, service: Service, config: Configuration.ServiceInfo) { log.info("Checking user-given URL: $baseURL") - val davBase = DavResource(httpClient.okHttpClient, baseURL, log) + val davBase = DavResource(httpClient.okHttpClient, baseURL, loginModel.credentials!!.accessToken, log) try { when (service) { Service.CARDDAV -> { @@ -199,7 +199,7 @@ class DavResourceFinder( fun queryEmailAddress(principal: HttpUrl): List { val mailboxes = LinkedList() try { - DavResource(httpClient.okHttpClient, principal, log).propfind(0, CalendarUserAddressSet.NAME) { response, _ -> + DavResource(httpClient.okHttpClient, principal, null, log).propfind(0, CalendarUserAddressSet.NAME) { response, _ -> response[CalendarUserAddressSet::class.java]?.let { addressSet -> for (href in addressSet.hrefs) try { @@ -313,7 +313,7 @@ class DavResourceFinder( fun providesService(url: HttpUrl, service: Service): Boolean { var provided = false try { - DavResource(httpClient.okHttpClient, url, log).options { capabilities, _ -> + DavResource(httpClient.okHttpClient, url, loginModel.credentials!!.accessToken, log).options { capabilities, _ -> if ((service == Service.CARDDAV && capabilities.contains("addressbook")) || (service == Service.CALDAV && capabilities.contains("calendar-access"))) provided = true @@ -401,7 +401,7 @@ class DavResourceFinder( @Throws(IOException::class, HttpException::class, DavException::class) fun getCurrentUserPrincipal(url: HttpUrl, service: Service?): HttpUrl? { var principal: HttpUrl? = null - DavResource(httpClient.okHttpClient, url, log).propfind(0, CurrentUserPrincipal.NAME) { response, _ -> + DavResource(httpClient.okHttpClient, url, loginModel.credentials!!.accessToken, log).propfind(0, CurrentUserPrincipal.NAME) { response, _ -> response[CurrentUserPrincipal::class.java]?.href?.let { href -> response.requestedUrl.resolve(href)?.let { log.info("Found current-user-principal: $it") diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt index 5a6561f0a..db67858cb 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt @@ -27,8 +27,11 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProviders import at.bitfire.davdroid.R import at.bitfire.davdroid.authorization.IdentityProvider +import at.bitfire.davdroid.databinding.FragmentGoogleAuthenticatorBinding +import at.bitfire.davdroid.db.Credentials import kotlinx.android.synthetic.main.fragment_google_authenticator.* import net.openid.appauth.* import org.json.JSONException @@ -39,11 +42,15 @@ import java.io.InputStream import java.io.InputStreamReader import java.net.HttpURLConnection import java.net.MalformedURLException +import java.net.URI import java.net.URL class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenResponseCallback { + private lateinit var model: GoogleAuthenticatorModel + private lateinit var loginModel: LoginModel + private val extraAuthServiceDiscovery = "authServiceDiscovery" private val extraClientSecret = "clientSecret" @@ -55,12 +62,19 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val view = inflater.inflate(R.layout.fragment_google_authenticator, container, false) + model = ViewModelProviders.of(this).get(GoogleAuthenticatorModel::class.java) + loginModel = ViewModelProviders.of(requireActivity())[LoginModel::class.java] // Initialise the authorization service authorizationService = AuthorizationService(context!!) + val v = FragmentGoogleAuthenticatorBinding.inflate(inflater, container, false) + v.lifecycleOwner = this + v.model = model + activity?.intent?.let { + model.initialize(it) + if (!with(it) { getBooleanExtra(LoginActivity.ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE, false) }) { // Get all the account providers val providers = IdentityProvider.getEnabledProviders(context) @@ -98,7 +112,7 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon } } - return view + return v.root } private fun makeAuthRequest( @@ -287,16 +301,17 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon private fun onAccountInfoGotten() { if (userInfoJson != null) { try { - var name = "Unknown" - if (userInfoJson!!.has("name")) { - name = userInfoJson!!.getString("name") - } - - var emailAddress: String? = null + var emailAddress = "" if (userInfoJson!!.has("email")) { emailAddress = userInfoJson!!.getString("email") } + if (validate(emailAddress, authState!!.accessToken!!, authState!!.refreshToken!!)) + requireFragmentManager().beginTransaction() + .replace(android.R.id.content, DetectConfigurationFragment(), null) + .addToBackStack(null) + .commit() + /*account.setName(name) account.setEmailAddress(emailAddress) account.setAuthToken(authState!!.accessToken) @@ -313,6 +328,41 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon } + private fun validate(emailAddress: String, accessToken: String, refreshToken: String): Boolean { + var valid = false + + fun validateUrl() { + model.baseUrlError.value = null + try { + val uri = URI("https://www.google.com/calendar/dav/$emailAddress/events") + if (uri.scheme.equals("http", true) || uri.scheme.equals("https", true)) { + valid = true + loginModel.baseURI = uri + } else + model.baseUrlError.value = getString(R.string.login_url_must_be_http_or_https) + } catch (e: Exception) { + model.baseUrlError.value = e.localizedMessage + } + } + + when { + + model.loginWithUrlAndTokens.value == true -> { + validateUrl() + + model.usernameError.value = null + + if (loginModel.baseURI != null) { + valid = true + loginModel.credentials = Credentials(emailAddress, null, accessToken, refreshToken, null) + } + } + + } + + return valid + } + override fun onDestroy() { super.onDestroy() authorizationService?.dispose() diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorModel.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorModel.kt new file mode 100644 index 000000000..d77217537 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorModel.kt @@ -0,0 +1,48 @@ +package at.bitfire.davdroid.ui.setup + +import android.content.Intent +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class GoogleAuthenticatorModel: ViewModel() { + + private var initialized = false + + val loginWithUrlAndTokens = MutableLiveData() + + val baseUrl = MutableLiveData() + val baseUrlError = MutableLiveData() + + val emailAddress = MutableLiveData() + val emailAddressError = MutableLiveData() + + val username = MutableLiveData() + val usernameError = MutableLiveData() + + val password = MutableLiveData() + val passwordError = MutableLiveData() + + val certificateAlias = MutableLiveData() + val certificateAliasError = MutableLiveData() + + init { + loginWithUrlAndTokens.value = true + } + + fun initialize(intent: Intent) { + if (initialized) + return + + // we've got initial login data + val givenUrl = intent.getStringExtra(LoginActivity.EXTRA_URL) + val givenUsername = intent.getStringExtra(LoginActivity.EXTRA_USERNAME) + val givenPassword = intent.getStringExtra(LoginActivity.EXTRA_PASSWORD) + + baseUrl.value = givenUrl + + password.value = givenPassword + + initialized = true + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt b/app/src/main/java/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt index 87b148239..793bbc02d 100644 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt @@ -242,7 +242,7 @@ class DavDocumentsProvider: DocumentsProvider() { httpClient(parent.mountId).use { client -> val parentUrl = parent.toHttpUrl(db) - val folder = DavCollection(client.okHttpClient, parentUrl) + val folder = DavCollection(client.okHttpClient, parentUrl, null) folder.propfind(1, *DAV_FILE_FIELDS) { response, relation -> Logger.log.fine("$relation $response") diff --git a/app/src/main/res/layout/fragment_google_authenticator.xml b/app/src/main/res/layout/fragment_google_authenticator.xml index eca69e0bb..00affa7c9 100644 --- a/app/src/main/res/layout/fragment_google_authenticator.xml +++ b/app/src/main/res/layout/fragment_google_authenticator.xml @@ -1,23 +1,33 @@ - + xmlns:app="http://schemas.android.com/apk/res-auto"> - + + + + - + - + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b2302a1c3..4208fe05b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,7 +13,7 @@ e.foundation.webdav.eelo foundation.e.accountmanager.address_book DAVx⁵ Address book - at.bitfire.davdroid.addressbooks + foundation.e.accountmanager.addressbooks Address books This field is required Help @@ -414,7 +414,7 @@ Owner: - at.bitfire.davdroid.debug + foundation.e.accountmanager.debug Debug info ZIP archive Contains debug info and logs @@ -451,7 +451,7 @@ Almost no free space left - at.bitfire.davdroid.webdav + foundation.e.accountmanager.webdav WebDAV mounts Quota used: %1$s / available: %2$s Share content -- GitLab From 8c037d647dada0e0550788f1d1c0f3817f54cfa3 Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Mon, 11 Jun 2018 12:28:32 +0530 Subject: [PATCH 012/285] Added new columns to the Service table, fixed some authentication issues --- .../java/at/bitfire/davdroid/DavService.kt | 202 +++++++++++------- .../at/bitfire/davdroid/db/AppDatabase.kt | 4 +- .../java/at/bitfire/davdroid/db/Service.kt | 4 + .../davdroid/settings/AccountSettings.kt | 4 + .../ui/setup/AccountDetailsFragment.kt | 8 +- 5 files changed, 142 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/DavService.kt b/app/src/main/java/at/bitfire/davdroid/DavService.kt index eefa5c484..5e3e66974 100644 --- a/app/src/main/java/at/bitfire/davdroid/DavService.kt +++ b/app/src/main/java/at/bitfire/davdroid/DavService.kt @@ -305,89 +305,141 @@ class DavService: IntentService("DavService") { .build().use { client -> val httpClient = client.okHttpClient - // refresh home set list (from principal) - service.principal?.let { principalUrl -> - Logger.log.fine("Querying principal $principalUrl for home sets") - queryHomeSets(httpClient, principalUrl) - } + // refresh home set list (from principal) + service.accessToken?.let { accessToken -> + service.principal?.let { principalUrl -> + Logger.log.fine("Querying principal $principalUrl for home sets") + queryHomeSets(httpClient, principalUrl) + } - // now refresh homesets and their member collections - val itHomeSets = homeSets.iterator() - while (itHomeSets.hasNext()) { - val (homeSetUrl, homeSet) = itHomeSets.next() - Logger.log.fine("Listing home set $homeSetUrl") + // now refresh homesets and their member collections + val itHomeSets = homeSets.iterator() + while (itHomeSets.hasNext()) { + val homeSet = itHomeSets.next() + Logger.log.fine("Listing home set ${homeSet.key}") + + try { + DavResource(httpClient, homeSet.key, accessToken).propfind( + 1, + *DAV_COLLECTION_PROPERTIES + ) { response, relation -> + if (!response.isSuccess()) + return@propfind + + if (relation == Response.HrefRelation.SELF) { + // this response is about the homeset itself + homeSet.value.displayName = + response[DisplayName::class.java]?.displayName + homeSet.value.privBind = + response[CurrentUserPrivilegeSet::class.java]?.mayBind + ?: true + } - try { - DavResource(httpClient, homeSetUrl).propfind(1, *DAV_COLLECTION_PROPERTIES) { response, relation -> - if (!response.isSuccess()) - return@propfind - - if (relation == Response.HrefRelation.SELF) { - // this response is about the homeset itself - homeSet.displayName = response[DisplayName::class.java]?.displayName - homeSet.privBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind ?: true + // in any case, check whether the response is about a useable collection + val info = + Collection.fromDavResponse(response) ?: return@propfind + info.serviceId = serviceId + info.confirmed = true + Logger.log.log(Level.FINE, "Found collection", info) + + // remember usable collections + if ((service.type == Service.TYPE_CARDDAV && info.type == Collection.TYPE_ADDRESSBOOK) || + (service.type == Service.TYPE_CALDAV && arrayOf( + Collection.TYPE_CALENDAR, + Collection.TYPE_WEBCAL + ).contains(info.type)) + ) + collections[response.href] = info + } + } catch (e: HttpException) { + if (e.code in arrayOf(403, 404, 410)) + // delete home set only if it was not accessible (40x) + itHomeSets.remove() } + } - // in any case, check whether the response is about a useable collection - val info = Collection.fromDavResponse(response) ?: return@propfind - info.serviceId = serviceId - info.refHomeSet = homeSet - info.confirmed = true - - // whether new collections are selected for synchronization by default (controlled by managed setting) - info.sync = syncAllCollections - - info.owner = response[Owner::class.java]?.href?.let { response.href.resolve(it) } - Logger.log.log(Level.FINE, "Found collection", info) - - // remember usable collections - if ((service.type == Service.TYPE_CARDDAV && info.type == Collection.TYPE_ADDRESSBOOK) || - (service.type == Service.TYPE_CALDAV && arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(info.type))) - collections[response.href] = info + // check/refresh unconfirmed collections + val collectionsIter = collections.entries.iterator() + while (collectionsIter.hasNext()) { + val currentCollection = collectionsIter.next() + val (url, info) = currentCollection + if (!info.confirmed) + try { + // this collection doesn't belong to a homeset anymore, otherwise it would have been confirmed + info.homeSetId = null + + DavResource(httpClient, url).propfind( + 0, + *DAV_COLLECTION_PROPERTIES + ) { response, _ -> + if (!response.isSuccess()) + return@propfind + + val collection = + Collection.fromDavResponse(response) ?: return@propfind + collection.serviceId = + info.serviceId // use same service ID as previous entry + collection.confirmed = true + + // remove unusable collections + if ((service.type == Service.TYPE_CARDDAV && collection.type != Collection.TYPE_ADDRESSBOOK) || + (service.type == Service.TYPE_CALDAV && !arrayOf( + Collection.TYPE_CALENDAR, + Collection.TYPE_WEBCAL + ).contains(collection.type)) || + (collection.type == Collection.TYPE_WEBCAL && collection.source == null) + ) + collectionsIter.remove() + else + // update this collection in list + currentCollection.setValue(collection) + } + } catch (e: HttpException) { + if (e.code in arrayOf(403, 404, 410)) + // delete collection only if it was not accessible (40x) + collectionsIter.remove() + else + throw e + } } - } catch(e: HttpException) { - if (e.code in arrayOf(403, 404, 410)) - // delete home set only if it was not accessible (40x) - itHomeSets.remove() - } - } - // check/refresh unconfirmed collections - val collectionsIter = collections.entries.iterator() - while (collectionsIter.hasNext()) { - val currentCollection = collectionsIter.next() - val (url, info) = currentCollection - if (!info.confirmed) - try { - // this collection doesn't belong to a homeset anymore, otherwise it would have been confirmed - info.homeSetId = null - - DavResource(httpClient, url).propfind(0, *DAV_COLLECTION_PROPERTIES) { response, _ -> - if (!response.isSuccess()) - return@propfind - - val collection = Collection.fromDavResponse(response) ?: return@propfind - collection.serviceId = info.serviceId // use same service ID as previous entry - collection.confirmed = true - - // remove unusable collections - if ((service.type == Service.TYPE_CARDDAV && collection.type != Collection.TYPE_ADDRESSBOOK) || - (service.type == Service.TYPE_CALDAV && !arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(collection.type)) || - (collection.type == Collection.TYPE_WEBCAL && collection.source == null)) - collectionsIter.remove() - else - // update this collection in list - currentCollection.setValue(collection) - } - } catch(e: HttpException) { - if (e.code in arrayOf(403, 404, 410)) - // delete collection only if it was not accessible (40x) - collectionsIter.remove() - else - throw e + // check/refresh unconfirmed collections + val itCollections = collections.entries.iterator() + while (itCollections.hasNext()) { + val (url, info) = itCollections.next() + if (!info.confirmed) + try { + DavResource(httpClient, url, accessToken).propfind( + 0, + *DAV_COLLECTION_PROPERTIES + ) { response, _ -> + if (!response.isSuccess()) + return@propfind + + val collection = + Collection.fromDavResponse(response) ?: return@propfind + collection.confirmed = true + + // remove unusable collections + if ((service.type == Service.TYPE_CARDDAV && collection.type != Collection.TYPE_ADDRESSBOOK) || + (service.type == Service.TYPE_CALDAV && !arrayOf( + Collection.TYPE_CALENDAR, + Collection.TYPE_WEBCAL + ).contains(collection.type)) || + (collection.type == Collection.TYPE_WEBCAL && collection.source == null) + ) + itCollections.remove() + } + } catch (e: HttpException) { + if (e.code in arrayOf(403, 404, 410)) + // delete collection only if it was not accessible (40x) + itCollections.remove() + else + throw e + } } + } } - } db.runInTransaction { saveHomesets() diff --git a/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt b/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt index fc56d4c69..b23e43d3f 100644 --- a/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt +++ b/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt @@ -116,11 +116,13 @@ abstract class AppDatabase: RoomDatabase() { "CREATE TABLE service(" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," + "accountName TEXT NOT NULL," + + "accessToken TEXT NOT NULL," + + "refreshToken TEXT NOT NULL," + "type TEXT NOT NULL," + "principal TEXT DEFAULT NULL" + ")", "CREATE UNIQUE INDEX index_service_accountName_type ON service(accountName, type)", - "INSERT INTO service(id, accountName, type, principal) SELECT _id, accountName, service, principal FROM services", + "INSERT INTO service(id, accountName, accessToken, refreshToken, type, principal) SELECT _id, accountName, accessToken, refreshToken, service, principal FROM services", "DROP TABLE services", // migrate "homesets" to "homeset": rename columns, make id NOT NULL diff --git a/app/src/main/java/at/bitfire/davdroid/db/Service.kt b/app/src/main/java/at/bitfire/davdroid/db/Service.kt index 0287f6c21..8a4a823ff 100644 --- a/app/src/main/java/at/bitfire/davdroid/db/Service.kt +++ b/app/src/main/java/at/bitfire/davdroid/db/Service.kt @@ -19,6 +19,10 @@ data class Service( override var id: Long, var accountName: String, + + var accessToken: String?, + var refreshToken: String?, + var type: String, var principal: HttpUrl? diff --git a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt index 13d3ee569..f030ab022 100644 --- a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt @@ -145,6 +145,10 @@ class AccountSettings( bundle.putString(KEY_USERNAME, credentials.userName) if (credentials.certificateAlias != null) bundle.putString(KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) + if (credentials.accessToken != null) + bundle.putString(KEY_ACCESS_TOKEN, credentials.accessToken) + if (credentials.refreshToken != null) + bundle.putString(KEY_REFRESH_TOKEN, credentials.refreshToken) } return bundle diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 074aabd3d..279934af5 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -180,7 +180,7 @@ class AccountDetailsFragment : Fragment() { val addrBookAuthority = context.getString(R.string.address_books_authority) if (config.cardDAV != null) { // insert CardDAV service - val id = insertService(name, Service.TYPE_CARDDAV, config.cardDAV) + val id = insertService(name, credentials?.accessToken, credentials?.refreshToken, Service.TYPE_CARDDAV, config.cardDAV) // initial CardDAV account settings accountSettings.setGroupMethod(groupMethod) @@ -197,7 +197,7 @@ class AccountDetailsFragment : Fragment() { if (config.calDAV != null) { // insert CalDAV service - val id = insertService(name, Service.TYPE_CALDAV, config.calDAV) + val id = insertService(name, credentials?.accessToken, credentials?.refreshToken, Service.TYPE_CALDAV, config.calDAV) // start CalDAV service detection (refresh collections) refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id) @@ -226,9 +226,9 @@ class AccountDetailsFragment : Fragment() { return result } - private fun insertService(accountName: String, type: String, info: DavResourceFinder.Configuration.ServiceInfo): Long { + private fun insertService(accountName: String, accessToken: String?, refreshToken: String?, type: String, info: DavResourceFinder.Configuration.ServiceInfo): Long { // insert service - val service = Service(0, accountName, type, info.principal) + val service = Service(0, accountName, accessToken, refreshToken, type, info.principal) val serviceId = db.serviceDao().insertOrReplace(service) // insert home sets -- GitLab From e10fd0030c8145b942e1ac86addb9ff88fbaa066 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Mon, 22 Aug 2022 17:53:14 +0600 Subject: [PATCH 013/285] Implemented OAuth token support. Refreshing of tokens not working yet. --- .../davdroid/syncadapter/ContactsSyncManager.kt | 16 ++++++++++++---- .../ui/setup/GoogleAuthenticatorFragment.kt | 5 +---- .../res/layout/fragment_google_authenticator.xml | 9 --------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt index d9d901bdb..0fe983edb 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt @@ -95,6 +95,7 @@ class ContactsSyncManager( } private val readOnly = localAddressBook.readOnly + private val accessToken: String? = accountSettings.credentials().accessToken private var hasVCard4 = false private var hasJCard = false @@ -377,10 +378,17 @@ class ContactsSyncManager( .build() try { - val response = client.okHttpClient.newCall(Request.Builder() - .get() - .url(httpUrl) - .build()).execute() + val requestBuilder = Request.Builder() + .get() + .url(httpUrl) + + if (accessToken != null && accessToken.isNotEmpty()) { + requestBuilder.header("Authorization", "Bearer $accessToken") + } + + val response = client.okHttpClient.newCall(requestBuilder + .build()) + .execute() if (response.isSuccessful) return response.body?.bytes() diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt index db67858cb..e4aeb89be 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt @@ -184,10 +184,7 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon override fun onTokenRequestCompleted(response: TokenResponse?, ex: AuthorizationException?) { authState?.update(response, ex) - progress_bar.visibility = View.GONE - auth_token_success_text_view.visibility = View.VISIBLE - - getAccountInfo() + getAccountInfo() } private fun getAccountInfo() { diff --git a/app/src/main/res/layout/fragment_google_authenticator.xml b/app/src/main/res/layout/fragment_google_authenticator.xml index 00affa7c9..1b3ed112c 100644 --- a/app/src/main/res/layout/fragment_google_authenticator.xml +++ b/app/src/main/res/layout/fragment_google_authenticator.xml @@ -19,15 +19,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" /> - - - -- GitLab From baeab8fce5a3d8be92fc9a1fda9a902509e5cd50 Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Tue, 12 Jun 2018 11:13:01 +0530 Subject: [PATCH 014/285] Don't show the login activity on the Android recent apps screen. --- app/src/main/AndroidManifest.xml | 1 + .../davdroid/ui/setup/GoogleAuthenticatorFragment.kt | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b89beb982..19c8529de 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -104,6 +104,7 @@ android:name=".ui.setup.LoginActivity" android:label="@string/login_title" android:parentActivityName=".ui.AccountsActivity" + android:excludeFromRecents="true" android:exported="true"> diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt index e4aeb89be..e4246a053 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt @@ -16,6 +16,7 @@ package at.bitfire.davdroid.ui.setup +import android.app.Activity import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -136,8 +137,9 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon idp.clientSecret), authorizationService?.createCustomTabsIntentBuilder()!! .build()) - - activity?.finish() + + requireActivity().setResult(Activity.RESULT_OK) + requireActivity().finish() } private fun createPostAuthorizationIntent( -- GitLab From 89c14b96578fba31a7ac8f8093b37f58ad1f6959 Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Tue, 12 Jun 2018 11:45:35 +0530 Subject: [PATCH 015/285] Changed app icon and renamed app. --- app/src/main/ic_launcher-web.png | Bin 23286 -> 19896 bytes .../res/mipmap-anydpi-v26/ic_launcher.xml | 4 ++-- .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 ++++++ app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3410 -> 1667 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 1720 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3615 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2239 -> 1440 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 945 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2568 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4391 -> 2748 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 2442 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5640 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 6521 -> 3760 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 4070 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 8379 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 8597 -> 5950 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 5732 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 12741 bytes .../res/values/ic_launcher_background.xml | 5 +++++ app/src/main/res/values/strings.xml | 10 +++++----- .../main/res/xml/account_authenticator.xml | 4 ++-- .../account_authenticator_address_book.xml | 2 +- 22 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values/ic_launcher_background.xml diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png index 4cc81409080c7754124235d4d31c11ce7a8434f0..cafde5df9d1e89e1bdc0c9800cacbdabf7bd084a 100644 GIT binary patch literal 19896 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 literal 23286 zcmc$`g;$i{-v#;%-QC?Gl1fN-h;&LLV9>}&$AH8Tk_t!&f&v1P0!sIQNJ)bm{wV8 z^liv2sYywr*kJ;gJgFSZk{k`e(|gaF6FItbH~&@V;6MkpazUw!Nx?6Y626|E?lMrg z@sO_L3rf#^ZsFJXO%t>RwMS8INT-{N)&3Ivkb82tJt=*G0)COuT`bB6`3 z8b)x`4-4!S;)kNqhA;b?`(KB_Eaf6Xsh*KL$6?)eY zLYOGtN7h8983syPO*h7qOBE}=K{Wz$XmmUx###1b}=`-CM{?Ud7a#Fp> zUrCsWwjM~_7`5-BMVcsGiUyC(@2k59FJD+WAR(dPr84Fe&W2A8%#RcG<}5+s?wqmQ|JZ6&a(h_(<7%R6#!EiFaDA!48v5?Qhh|tEe4pSh|v8 zkgorMTjlu2+>dEK7Whp2Aw(=S&K-N}r(x zxEWE55dpp9k*q4n&26QddN<07zla3%AwV69h$rV+g_elvPPQ`Ql8M=FXkdCB36_!`xkrA z1z*0fc%PlNoJZop=;Gu}H&jFyzofja@ZV|MwWvf#IT7dd8&CcX-1rvEHY|4kIbHV- zah)?Pt@&VxX@}}9^iT?>qTm}SOlMYfX--2*sz+a^!oezAaKz=azl2VlHj6}B3O|BP7lvQE|djUIHjz@QEvTF=;IPNFdFC(?~ZGaO-F zxm_lB@vL!FdVZ1^Hkvx#leyW$J;Msh>-Zfs#A3?Mi_O(hEzal9zZ`o%04tMjDE=;Y zcFaq>nj_0ywx~8q0IcBzUs0#5k zc2d1caz$&AqGw`c=Swh~%QiE|w8&gcMIs6McN|rhUg%Wt%1?x1j+WX(0zL&ie^}FLMvFub%gjno>p@q0J@Vk0F@B$b%) zy8pmY=W22>g$w5{HsS*sBJ?!*2K)ZR={DE~G|k4ZXkV@2T1irRaG5$>2=6Tok9r|R7q=_>olYVd~IYgZgiVQa1@e9=)nxp_di7HcpO93Yj>at zi#NHN;GX{bLS=%U^P18XP6?SY5I~^&31`V4&J

=`J@7GtVO-rNfjD<+^5U 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 V`--=zXLU1yrm7z5y|QKa{{WyfOz;2z diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 476888c8a..565c55fb7 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,6 @@ - - + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..3383a5256 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index c402e3feb9efa131b521651be72c2a40e2b73c80..5f37a53ba01c4cc942e4f63fc5442299388f2d4d 100644 GIT binary patch delta 1651 zcmV-(28{XA8iNgxBYy@;Nkl@Z@E{}SWLT<%S>6x7E?xw#h9rkLZ8uht^?CWHb~PI%@#ud z6Ihd6Y9i3iQlsi=N)JQL_M?UX=RlePQ)L8@|-#3p(yr*S=l8TMAHUHrp=;{nWehFWO z=W~G?1PD4Moix&xk5(RnuFe27Z`E;sD3(*Us%RNxMP%;k?$=IQ6?$hjn{YJWLI7uqYSwflSet>*$KT;m>| zfd{;(;wFt!Alkc~D}3D7$gPtdux zQkt-iyMG*WH?3zIhcOMA!%-MLRw?+Rr0&sQ`f&bwt}i-|S@pJOU8!IFHK3J>YMC&X=@H z0v_->(+2ux?~m{ZK*-fhN^}ZsseibO^6R!bUcnd-c%2JHth|q20|*H%IdN1cTbT;$ z<0;j0tFwrEc-ASndf5{C=rw>4Gsz7$hkr%~=zN=n4xde=3>IF1aE*I-CRaZC=oNsF zH%4mI)#(dYr$^Ao*>c=&2M8em^2hE@XUHY=akh{~2gut1xpImjTJIa`WCrSBad zOKJqK*eNE*NZ29FEMt^$M zWu!%yc^Kc{`z}*SV-{td*+jd_qJ;L*P^Y9Ro7(%EnXCzQEb~$wMNFTQ7c-qouHTxq zT*pA(C((KIvZE*^)abb0O1ZVMF5f!NtBs>;16PzCYB&m3VK?^Iz3R68b;zE}HhDc* zb(HRC?5K9@u|3E}8;VVLI7*mZ?tiKiGRC3=*=R%23A4FUuHzG9Q>jlkj3UT{Y_y^1 zl&hnpJ@&P*w7U`{$bw9JduEDC+DqB?fg*7DTru4s1G4OaqsVO^DI15F3XL6SD7*Rv zr$!U%pe|(Cn^p?mIF!Ei4C7hddW3SSUz9YWzyn^?nQQk%@mtTLhZhD%`hTgptDfhZ z3+rF!4&D1zPjkXG?%^4Dz^jSF3v!OXVA>4dyur!d6w<`;7ySU0egscH#Hk+xRw6+9 zL2{M8iXaQ1_|H7|u7W_c+`&bt~%A1Tl(K+~^7#pcNyLD^b=p)MwKMS;@*#bky) zKP`-bq@~frbxJ%ILF*HiY=42SaR-*q7k#PsHbxJ;R=DaX3`Gdk5^?*pFSLp27{qlI z{m?*kbTk0+e`r-eL~L$Y(;Hue(~e>TL9nWcO{)Z^OH5YirG?AxH9<#%L122#EWyFS zx3ix>Mj=sgi;|u-hZJpociD+eX-ki9-n;ax|52LFb=oY?r0}>yCT#oVeP(@!4$uWU zLARiwpgZJze8D#`Fz`9A~ delta 3408 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index 77e9d2fc2b06a484953880f13d171082c4baeb94..0abd7b9aef1eeb349120f5ea7cc09164fec65018 100644 GIT binary patch delta 1423 zcmV;A1#tSm5ugi@BYy>FNkl3_$`6M8>v&2rG(Q#vP#) z+Pr?x0YVGs($-VjA3MpLLsHK7JE34;<_P$W?umGLlQ~vi0{aqP z-4LNs4!a_OU7;^2tRnD#-$0BukP?b_(3fR<=v?^$I$OSvzWDh)iu-9B`(0%>fgPWC zz34Ck0G(8}mo&{u)Y4N)e~+}$@V~t@t{hD=S6-o(Fg#Kt|&%we{6g`me|6WXVpJ8y`?{YbuRT4GBJj z@Z%*RH-7;vhOiOxywPN&hI`joY`?YaxsH1Q0&xuu=r{ zGn1@Ao!||l<3&NNI@L+8F*9qRwCW@B5St0|7X2@sDh+`=`hMqP>3 zeM9^Ed3g!URi9oPy6h|$PK~-^y47<-vnC4i}NTq618wEC_>c?n=7##Ctw z2!C|sDZ>OAX7x4`sO{9qO8_y9kyt8$+xzRO^v*Z*ZFMM}E)Aym;&)j7Y-eM_L9T<> zb!pi;I<8}jtyOk6U0wnZBbev;Hk+QAV&g**eO-BoCGIwonqW_s?4q2e)AY}1haiEh zhB&$VfQl0%d|uD}Es;Jie8WcNreY(;zKS3%@aST>6GK9QOH2^W`wSZren`r zNMwcK-ry|@hvkVZ906!pOq@%Txa4-);%k@E(OMN3<4eICX7rR^d(82Q41XmR zbN^By)85}e7k@b-I!BytP^@;#%8GiBz`SD;XiAuekR!+$p~i)f{IalF=`V8t0Dq+gB?U18uZu}W)OnOji)te$0ee60LIy%fL5!fZ zVkE{?iNOrB4aNghA8V_XT_1N3oblf1s-M?`Lp?nO2`n5M>^$nVE+Wr=kSm3*a=^j* zMtGLbP~h$j9>Azn^AIpQVtV{=>g(B&d2$5>#ev77Xu$k|Ly*)hL1A#@-`%6hE6pF{#pKF*s>1102EOs5dBUwI9^?uZuCYZ1f dE*H7}0~4gb`ns+~DF6Tf00>D%PDHLkV1lxevxooy delta 2228 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 RgP;^hfv2mV%Q~loCICfDz9;|y literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..2717d05c911ea085137abb8cced628e0af447acc GIT binary patch literal 2568 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 v?oK`{ literal 4391 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 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..76499e569a47cb0879b32919382c62830629a7fa GIT binary patch literal 2442 zcmb_e`#%$k8}`-FvnbCqVOy=bF@%8zvEmB;s#Y$X$?Z)9FI zJu4WpUu%^~Hk^*8mG`k3SWX`sx0Gxe`0MdZWmb(n*=C$9g#+Mlgt@CP#Lm#o#CawA0#FbNF=WO7Afd1ay-3#3njkn?}O!RPdW4k($ zP7VL4eepwyRfpek@!|M}`&!`*G`c?LwD;pu`4IHco#6M>%i3}Xg}^ZVepcd25ufuw=?Ya zLm&Dh-udGLX6Du`JOScW2{?d4Gtb|DX*XH+t$STE;0qoRe7sFh8%VN5Sy~VjK~r^7 zhw2GOTa|5*1H1TX>3b-^@U&N&P~~!JC+qTW8j{YsgD5v<sK@$ANNbnDtZrBW47e^I9cEy z7ucjkcP*E{Vb79)qlrcB^9Ph7mt0c>V6R5MS{fZqv$0-Pij=k;6LXU<=sYE6n&k_N*R>ApIpvb76&uSBj+3pqj%?_KopdS*u&6d z2T`oD6g96(?9C-U&f;X2wv&J8FI~b=k7s((baOwIl@5QZ4<0r*({grP5>*Vl-ONXD7}~3Z@sRd4PmFlX?NK>8q}9xG0@gA)pKo79 z^AP%vZnlRw6&43kti_7LxuJ>^>DNMU?*xrE)ei4gmPazZei52i?(!9oc?l_6`h=2% ze@s0Y9b5Z8R4V6`MW~PJ#2t+!u!b4{m(u>s6l_Pn2JcJUwq+)y;8qIx?&Lw;wa$*D z!zgr0nQhJq-k~2TQM4>kR?5^o6QOHhQBXx+iY-iFqHWA$(eb7{$!A3_qiaZ zc~qByz`F_7B6=-!%J3_*G_JE8fjWR;47Z3pt0OwTrS$rhvVEsTD0Y zT@JN2kzIIj7+USQ8COHp71M)6+|>09>YNq)k!}Lw`mmBfrSTJXQ+MO2SjR-_zIQ@) zE6KQ?qMOt-Bv=5*Zk^9FYD=KKqmEbS6YrXU?wXlE zW^|mN)r4<;&kd}Qx944AAAy~O==XK2i5UdQ*D)hxE;i>{;=X^c6cs07JGzTzR49ix zU7mb7JXK1@B#lgIOsGVpE|O1DzFw#4u0CYaNnd&PQI|^)K{8g`{9b-3K}sxX-nmw< z+3yPZ%GF0AsudR+2bVH5IPunm{yW;M)^CdPtH?pw88~{a)FTFZp=ae@#m0U^;R|F) zwd%`fIkVqUBsOW`y0^SoE_847YhtZKGV3+HW5k$FtgvD|~@a5j` z_w4iVfklncFK3*BePrdnXo|{|0fxckD>VdN{_nghk0^hR0e586-&)v)^ z(kxjc`YG(-OA0yr8l^}NsX#es%% z8)1Ny^vGXtnSl2C2VEas_h}@h*$Sk_y$30DGzY_rEqlu0rSa~I)cvmvLDQP1<=1fq z7C2PWkEyS=b&FlC)C^l?sG++GZT%JYmMTI2t UI4vygv-!0T2K92OIUbVwe>beMEC2ui literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..526a405c5764aa12d527092c21affa398a3572ff GIT binary patch literal 5640 zcmV+j7We6iP)Qn3$D|S|} zM=Y^N#U72Zmk=BF1~_lN*|~60%DuaH0n0sS4zTRrJM;bj{QaLbGkbr%zw*6^Suw3v zYmO1LBB*#?LwFxP^By<@F9zavRS67$YUq6!LHK*d`FTk}4t;$BN zX(nk7_#Lm|e|S$`1j&5H`yG9|YBBRZb&@ksxg`U-!~HhhMCmfi#R!aZLTYew#{H&?{uu<$6| zC)I`}1O`*UW@r)ulPQP~=^&bQS!38bwd5iG9KY8Q+#AopvvOpFcY))@zz^0nwW45s zsYwi2N&!j{1D*+{E+C!$Ov@bjTR$>h8RhM#@#+@} z0xpdpcqV@y-xaBBZgh@fQTfIyYccRc6MZVuEkBAb@?IwJS>mD~K+l~RB2mQ&aSwCF zoO4>s8^QH3^@Kl%CkB2^IqSTL<6o1_MPg(zgnpiM&coIPbP(3T)>PDYgW##+_efn} z6>18`WdffiW=N49S>lPXxzjaZEt%F*QFZ9VN{ddqCAZgwg{2jF;9nnUSz-zPg}P-< zYb|o}K2oP1o{j4ZSH>H!Bj_3gizy;rJ%TQ=Sdr2T!bHFoRAw&BQnGI(*XJUZvUWq{ z+JHqWt$xjgP6R>O>K&?p%}nG7j?vo!zF0TV!FIx0)t1ni9@b(E3XSu%YgHBKr1o=p zuee#MNeCn(H?|^Uw+55R+q;kj`-YG;$>YhUlQYS-KNgUrGYiSq)APvs6I03ZL!-#- z-F?V}Z6W06&0iAj`W7ZJTlG6ZeW2eQZ<|8d3Bm&h&t;*6eR|GUR^$Nmh5IAOu5-)D znUwwH_QPxBMcQ+ck&%(_c=G%qxt4mC9J`uCHlCbLrtauYh9@>tR3w22Ls$9j|6c1} zp+JcsSO84s>(rZ+1b_6Vwv5A*uWTjvpWLE=Kcj%AO9DN&k)EDLp1pWNQtn?Mn@`Us z({}!sj7V&*m|hsX!f%U5s9p@2+&U85z#5%8Rtd2KP8zkbHCcV+7xLH5BaC~E0X$D) zP-5PFltPYN*+L1?gNmrnj1ot5?$oJNlRJ_=6?pKJALG=OY8|$Mr=`Ikoy+)_{Occ+_FjTQl-4RMo zUjL0eeet-!!58m{`%i9@&8KFQpEkEM*g9hRV6}?$ncKLvNh~8l!9*?%tU@1p#TYw( zctSI>=Ezub^TCxu1OC+k<$gBh0GX8Zok6=HQWubp&kbDWceU#;UUteVWdxw~L8xN% zLCcI$x=^xv&aWY9=`V^5_~McJIEBRh{v%@&19VwhWW1X6TjaZnVr4GEO&RVV8m|f# z#dc)N{per6r6Olnkp*8E51-y8n@-I##8P?*e1@#@Ct)+ZJ1VjZ1cfqWMKf#qfOE_N zF(8x-&cAT$C^HF*GWcSVlz2RbM9CzAAd31g^*!G~>ru&QrOZEEjB5EGL0v<@2j>HP zC4hPU;wd?QGns5SIgL!GW+Vs;Zx>#2EcGAuo?lD;xqnHKXJMyq`y7u|`FD z&h{P)>Fd43Yhm`&E++;eIGGr4%mw zgh@%?%XoGmS(`kb$#bKLKnXc}cVF3}i0y(|-ZOxqY+WFHpznND4Z5Xn%jJI9`7kw= z0skbKxGlsWUjr5m#FnvhxN+|s`Cn2edAmTb3+{A?@TkLlgPAgX13~r=UF|W_=g>obe8_vymJp5@i42 z<*K%F?S4!SlT;$SfLP6v10xj3Cj?MJZc~IIZruBe{IVrTA_4S%!Qd7Cq{{@)Z=~sh zEbq;H=o-~xxgiHcHI)ji-Fb9_jNAIPBJuk*$>S9B5jLEdE_oL)?|7}6gw66^Nii*+ zuXikLk+7+as?r-=ldF6YQdVlA*HiyAC?t^6nDtw4#XRto`xjZpO^!+0Z;{W9w*ReD zgNgQ_Jahq%;uRaP%(uN<@dKM5@qDESaPii0#WGmPh(U%c<)CdmIYYJ>>NMWtJH0Y? zDV3k~V?K1H|5Ul$k4%=5$&{;i&kzw47&T_>>M1#$DODFp-JKe0K}M`qk?t{G^MGxx z)=;K~_Ao0M8L$3bE&*VsDH)Hs{qQ>Zc}sgm2(a?-Xhr7+^B4>#RSe;0_`D{`X4TBf z<|>8+>!-)GrjAtcriwo-C4r6{u9Ay{2O2ANk@;9=2QRKyJduNN$8aeoGS>tB7yI68 zJILJ~80YAe2?Pj__4y)m>q~UNP`#i}peq@bOegy<=!{mxWNDaGYS>4>q_3ptIa)E7V%gQ4Jqc)QNUnIs1R zF$9laVxE9`#a*Hsj>=gNWZ9=B2Cej;E64kV_(9!{sbceg{Jocq-SU+|v7oto29U?k z@0)CH@Gm5XsHq|jjqzMUF)sOrh%F%@`q{cq-71ms zsta-m0CTLMQiTtm-eGZ_Mf-;_1^C1I7Azz7hJN*v>9-zEPe_kN-xt@BCI-qT& zr7Ec+?z5oDWuKs0)nAK_3Iqt&)U~JHTZ$YL6os;dlec|F?mqsT+N~%fgA#$MEqSPz`e&eS#f}L0{A>^9_s9s8v%+353gTOE&))V|E>}M-aq0@ zatP3@t5aiOog)Fh98%9kt_VVN#JfrWWZEP-iXsuD3UO?hD*;;etn+bXygVh0&XjkR z07x1k93CAy;%Sy<%MjomDv^XS<;_D%A|_J+H}S zXO*o3u+CO1#2qp7@^% zMJ0a9f^ai;^`bj8hE+a4cydSam=JOnKTsb;sv{OQ)B9vO+j3Qz63!Nq=6PT`$`4xZ zzg12s0_i!W5;5S|BUgT9(bbU~KUcKTZ~U*{uzscR-Yk& zhiIabqDKA*pmuy1%kdf#mx|u@t>lEvz&JnBx{pgYU>u~)Au-RhG^Lt%t>ZyW(K~WX zQMpwIkimy8ZzMl%_;-=$f`_t38>_rL#Xf1wBbcBInx=ZTsp$)hbFLHB%Ow26cJK=S z-{lYhP0(@{0FILs#44a6RP@Oaotsb1l2)L_F$|6II!3YmkYSun0gJ8{y&X!^9~Hyr zHJK^5DgsT=QX`U>d{H|K0(piTsnSHzljjeli61?2SvvCDk?zQbRpc1we6^U&{%QH` zDEIF~)Rk121(zHSlavK4P@vjD>473(sWqghsiP-_`-Q@+0ZV& z+C83PT7_eq_vI4AOr#3Ik!~IJPK0zZ1UZWKe<`})?&B0PcTay~jtNj?AZ)SoTpTMi zk&=aj6DOxGwEq%c(!ATpVZby``TvR-Dr;T2v^|wwr{u(iFc2J*nh?M9=sKDGTX>;@ zkA%{5NkwJp%;A)J29bbS=oHVhmLHTtvtHSJ#gA}#w5o>EiOQWPc>Kh6fs8{rz|lB+ zL29K0f^N$WY9$BSloUHsIbWZC{F^bC4DQbuM4hj85p^d6OOC0ui;JQ1^#ZDT4qoAx zBG)8E#R4Lba=gm;qZ3$_LqXX5P`Icyk_tXV54?T3t*T+O8sDtl$kq>7I<$}=enc0m z5u!h0Y~D`DUOe;46(= zEHLC)$~!lLQ>0# zp>r4$m+zdCS0>N7zsHQmx9q(tsexe)RUWo~Ub~=@Vn# zt(+4&<9GhKOn-!ofsGpQH5MJvkYm)uG$cEQlFO+(yURJojB!5#Z|w#-PXl%wL&Hu@ zadBa~z{avlsk-6wo1T&D7YGg-4!$rn8->6fD$sWPxs)uW${J-LW4C_AxEch8^2Rep zZEV9Dy65cf%ev%`@N?&kNjFL=ImsdM(siQeIqOPg;dt3dvi^^Wlz}FaXMnvCO z9}?KvegH6Y@UkgyF!zhD66MRYf>QfV6B@3DC_;wEkgjzce4)f48m1f|sw}Z1%6fr8 zgQMyt04t7}BBy2*tLDLM*s4`)m2cF0j`sz@&6eLV5y%d{z}X9hQOlX1sR?UGc3+x+ z1rt3DFIE1Rl{c`mbuqy5$>TDw?rM!MG^idBV*mYsCBA8f9A6_0(Ya-MWCGdnX2vvOvrYpD}5rhXip4Y=HJy^Zlo(f$v`T2SZ7 zAsUOQCIV}Qo-Zwg8xZSnA5nKTFk#r}TTxB+O&jJmHcYc4wdVT$_=a1M?S}#~ln4SL zP+x=&bnFO(2$a?laPpAJLNW#Md}E3M^lJ@BN9SXQAKa5D7H+_VV`K7f*eGsYU9S)5 zMsx2nvC&>7h#+>}kTF9aF3ij4Mz+5q&IE2p8Wd?nW5eG9_}Kj+qw61_7!7W-qTga4!q1jYGX)U};pY2 zSYjtnBA8V)v#wCJ3{Hk}vaN3A6CC9h-zT;yzHfzvX@zKEQ3jqLKg@P*`240MXqekN z+m9@f)pBBMt6)~4sDUpg0?e>-Hq|UXqCacHJ`U{Y(4U&BXP~_J_i<%lYb#;0DmB<# z^k;o$D%#G`4bQ5+sX36___g_(%JDT8>8~_|BT}YZ<#JU@TbIT+lR8_Jw`kmEfJ;pG zsh%m!Qqq5E=1aCC;tNSfa2e0rhgb;-lIWo^o++RAb)JSfWA0c3)`B$^gp;MLNHR0C zs#wjUI^PYhAGz3k`sEPU`Q2l@{sxz^Tr<}Z0D^I%&eS4C{gR8BH2z%(jN>tmV!ks7EIof>2#f>B8iaB$xFa9Qi|C;9v z`AM%T*44|@;)HO8C+QbltIt;&*R`QjJTC(fBsDU3MnV=*^r4RIgAs{~k!-K8(<@T( zJwOl;8h%P>jMpVR6VIk|@Zucrh`C{oT-KL((}B-PHk3 z*1eshDYq^U8B>3ImkAA%yHE4{D{Ok>>lD;~de8N~je*}O!LH+f)Z$7G`KkW)V2xW` z>u~32|F7-CaWCAHgO1Zm9Pvy%o7?nd-v!`v(-EvE0Wud#4>hSsY_Ryc9xOKsoI8Sk z5Em6}E&Zz3w)w=ttx_vTuMgT_;CH-M(bmcr@8R!t=Q6!6zn25%iPjT3|O00L_;%dZ-@uJL+rW8a^!$wIEh9b{y)6O9-qNyaSzrUP4CG?Olkfs z^LHJ1DFi{R#exAxy=+t(P?1py@*1FK8}v2&k5wbk_j0f;_&weOZX`hjqL_1$QJjM+ i9>VM9BCv}*;Qt45qm>fW++@4}0000*HvS}>DN1WlxqeU^H4diWpZw{ev$H8&Blw5?Vbu1V5X7dA!Z!WQB z#^~vs)JsoZIlU|^`J8&-4XFo_{-KE6=FKkTR|%O_RbmM#E7L#9&${#^i)mo{5nsKR z&9{V6^A~fzjO?v6MmOKHy`|&;_z*0(C&S;dR%y*b^SVuj zI&1ES@y#T;IR74rwWP6&lZ2jMLCQ}<5j9toU%U6iWE1FDG=?Ey^e=eK&zb7t2FmB> z%9S-1X*jda9+t7l%Bw!FU1WRg{a!ut&U0sA3KQ4KATp=cO6{8H&j0k8z*~Yb6F|@3 z!<4%upK(~YVv8LA=g*Z5&+Q!?IuE$8S95dt!n>{^8AOw~UB%Z_yGf)krQz~gCq6&Z zM=O(^hvq2E`7`BQyg%e(Mr*?FzIcCZczVKRZE%6@B7{sGP*uX)0Ar$*k9i9nGU}=r z0SyudW4eInP?e;ik504ZsFz+8Sj~v)&X3W`#YKC}V=(4DD78i8>)uecAVs_UCQyPC zyY7p$VFP3Q0LiDsV)Ici8+#j!B7i$IZH6(x+d)tY#xPF=;ILzaCo#Y^=?d`F(C~Yn z(@nQK+#+XAWwYy7gGsozBMCB^%V9_EVxYbe&{gp>2# z#mA$3pWH|~6(u3Nnn|C2aV@?kBP+_PkQ#%V~3A$$=)GO0Dj+YeL z>7wj9HnN6N7pU$o9JO~T%KJL&m#UOTl}X^k_9FVUfd+aMbi{+Q;BGs`TCZqWDco5( zfT7xl%e}?X!`-RaSB@2y1A|TneDB?N^kMvA>DEVc0xchdivU(jwlfhULAj3oz89QS z#Qm!jS7JV5vH=7Xr4Z=&Au{oeyIRg_I)Kg=mS2fIHIM}zYoL-<*bo~qO@zuzCRv)C zD&`Y&w(&TiB;Lc9e~2~LY#HyBP}+46DcYttPWMX$FkZW%wnTtv7C%xI)C65pDb}pu z-82M}Y4bvYPVkU?u&I^7*&Q=5hI)5}$6dyJDL0v4R!u8Ku)QTSJkS@{vZT~*y%#ZV zp&Wn82q#Xu9jOzdt|Ohf?6C6YB&;=zuDy4s#f+RDUk!U93T!wlbSq;#!mdyE&*FT> zwzV27H##a)Zndx-E#FX=H&Gt9OLyMl1wM`QG;-O}Dt);4A^*O_X}J1pwvok9TRVvg zl}&lq7dr>QrzO5gL&WWgdQ9`lis3a@>p(Yk^kc+jMA08lIvg*5_$9baVcL76cxSr5 zBrL0KAmppPMybVj7t!z0fF&AtS@vMC;qtzp!+5w5<9VNL6^k_vQ>Q&ZG5d96lHNv= z!KnT^Pua()wjk!w=r@yra0MTLOj1OiF;KP!kuZf^Rsv0EHA-?e}rjEqwZ+Vi_Y|Zw=)dS2eV5@5ou1cOMp?M zet=I(5dNSkLq%9ulLp#jR!NVOl9F*5JVK{2LeGb`f*zvN#)0bj-H&uLwfl)4etlkk zH!#hzwRQyPTvQObVRggD&2d)x0$kF`%3;(uFV>vGjAA#D3!A)~>BsHh*7yMjmHJ4e zF>kNJ7R;?ZC5Egi0XjP1iN&j4y}ZUC_5mwF#~AW~9nht(O&)j{c&cljm-@vrwHhnu zsbyxbfds?*s-wpxH*=?Qu@4ze1DNoYAJ3ttVka-5H481KSBQ5Ry||c_WPz>jmR$bIP2x z$B8?8YTHWEURHsLL5FI9c8FNbRo6-8p)xsc0gr{%Z8t;tEpBTh{>Xg@92Hcv)Ba`^ z6yr&Y(D=2g;AWK=RO1(ra9$AxDN}e8 zbi<`}6LheDGCK~xaHE6Z_pu|PBY{L@6=2a?RLpZ4B!7$Si+p6uF^DB!vr9S2yQ(so zC3@S@#Tz|@@HkMiH4f$2!rS!=(f>WQ2&E7deY=TiRDJxB!h}2isEkGO;?y6wqB>{C zI+-O_sxkK)O|!+twW%F@VYHsoZ#~xxfoTg59|&SI1zboHyN3xOvNT5z_9qdB3mn zf9dn2J&{tJC?JF|gB0GpMMh9Iqf^u`4M-}_50V5zpHDlwRVYyD{!wQe1`0_6sw;G= z*^w>7uesgb`p{4BL_nz&Q|BbHc+G)}&R&3a#CWt~DkN2O%uSE&Lj{KhK~yyBvDju- z=|n$k-hyYVm-@={*pw(EHH_)<&9mLG_twa}vHmU8nKswPCgdn_h9#?hk; zyZ;x_kVc0$z7f_W(YmMkaVmQ*NL6VHiem~yAX})uK%S#++*>tt$WyL$vhcJJCE`JSliPB0X2W$=Rq5D9d*IY*lX@EgBXs%gq+J9 zzsy^YH>QBcW@+`BOD8VHH!kiMU#d#KNDsG4yk9fwlDUvLZQn z^Cu~eWKU5}gZrwFp2<@~<)>VKYyy!Gr4>?TKZJQ-dsxZ~AM414il8;xB}C77WELGt z2inrasbdB`f3TxAH!9FXn&MaDo##yMC-{Q_9*ec;i9eR2UKCEYAn#@UKmEHg2m_%P z(3Bb$V|fGHk) zvq*x<|J|eRvk=yiVu9-Ghg>Lb=1}@pZO+?bJG9sRKYJkkCWR9Ky<32Y1i*o!hf_mc zm;mDEy}7W*yVvRAleD96E||ai)2(`XAA%wUg^7Oj870=iu7_-MqH0(Q#s)=RQqC5w zU!Tjg7@~lfMb2Ac$4T)j{cK|?vI!VodF4V0_sc6&kbsQ37Hdb(W_Q6ti|o0{zA_YF zOl++3;?&ymBzXkIo?_veQ^H;SDDlbCQ#F3&wsq&)L7psAeo4@sBf`BSDtI^VVMOpg z-V=;jeJaF@;RqLJpq#%vQ!#|M6ehHn*PG9o(+YdR|RnO#)Np*=uZm=JUU%OnRIV+J63K z8c~Dh5(E;pj_d|Sr|U8`4u1DG=L5N3Snt}6hG(~hX*?Y+VP;e-yhdG5`Q{G`CC+GH zZ8mrsJx05Buj{X%Pg~6ToiYxew4Zo6(ba>7W+Kit`pwu8bf1tSuD71Rk<0r;(D@6@ z{+q4$?YL*sVfh0%_QVY*f8T0|E1Hg$mi%M3F?0s=#y8xqH%{wpS7y=tZG6&$d9nlL z!*9`mu0T%E9NaLg_WM6rrh9V~%kAfU|I7(ddv)t?T-<*!x~#3OJ$JIc&G#mr#hnON zedb#rS}2n-%;&AjTnEuh(;iQJVlu2yqvo&O=3m%G``jPS!QX37XV07a+|(ZHRk<_S4e?feb+kZZJ?}(9={JzfwYsn_(pD19g MZ>m?T^C0Sf0ByPuKmY&$ literal 6521 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..4baae2652887ca25f7e51d45b774a72f207ec193 GIT binary patch literal 8379 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 Y4_CG0ob=-4)m$9VR7I#%DceN+4^iR-0ssI2 literal 8597 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 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..c2d12ad0a079346f39e2459278c5a7e21a992092 GIT binary patch literal 5732 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 Gv;PK+kNOk< literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..0c826f57c67d457b14b57d77886401695a586001 GIT binary patch literal 12741 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 LNkvXXu0mjfgDV6? literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..d9edc26bb --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,5 @@ + + + #7CB342 + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4208fe05b..e8a29cacd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,18 +1,18 @@ - DAVx⁵ - https://www.davx5.com/ - davx5app - + Account Manager + https://e.foundation/ + e_mydata Account does not exist (anymore) e.foundation.webdav + WebDAV Google e.foundation.webdav.google eelo e.foundation.webdav.eelo foundation.e.accountmanager.address_book - DAVx⁵ Address book + WebDav Address book foundation.e.accountmanager.addressbooks Address books This field is required diff --git a/app/src/main/res/xml/account_authenticator.xml b/app/src/main/res/xml/account_authenticator.xml index 85eaa2033..f32fccaba 100644 --- a/app/src/main/res/xml/account_authenticator.xml +++ b/app/src/main/res/xml/account_authenticator.xml @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/app/src/main/res/xml/account_authenticator_address_book.xml b/app/src/main/res/xml/account_authenticator_address_book.xml index 6305f31e7..e23f971ea 100644 --- a/app/src/main/res/xml/account_authenticator_address_book.xml +++ b/app/src/main/res/xml/account_authenticator_address_book.xml @@ -1,6 +1,6 @@ \ No newline at end of file -- GitLab From 0c0c9f6fcf41f4a19a948f8eb410f4c9cbaab4af Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Tue, 12 Jun 2018 12:05:30 +0530 Subject: [PATCH 016/285] Enable syncing of account automatically. --- app/src/main/java/at/bitfire/davdroid/db/Collection.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/db/Collection.kt b/app/src/main/java/at/bitfire/davdroid/db/Collection.kt index eee29ed14..213f54849 100644 --- a/app/src/main/java/at/bitfire/davdroid/db/Collection.kt +++ b/app/src/main/java/at/bitfire/davdroid/db/Collection.kt @@ -61,7 +61,7 @@ data class Collection( var source: HttpUrl? = null, /** whether this collection has been selected for synchronization */ - var sync: Boolean = false + var sync: Boolean = true ): IdEntity { @@ -171,4 +171,4 @@ data class Collection( fun title() = displayName ?: DavUtils.lastSegmentOfUrl(url) fun readOnly() = forceReadOnly || !privWriteContent -} \ No newline at end of file +} -- GitLab From 81f68fdc75f7e325916601d5b2a2c37a3d573903 Mon Sep 17 00:00:00 2001 From: Sumit Pundir Date: Fri, 14 Feb 2020 01:18:35 +0530 Subject: [PATCH 017/285] Use OAuth access token even if the account has only one contact and fixed issues with caldav --- app/src/main/java/at/bitfire/davdroid/DavService.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/DavService.kt b/app/src/main/java/at/bitfire/davdroid/DavService.kt index 5e3e66974..4ee9b1bba 100644 --- a/app/src/main/java/at/bitfire/davdroid/DavService.kt +++ b/app/src/main/java/at/bitfire/davdroid/DavService.kt @@ -190,7 +190,7 @@ class DavService: IntentService("DavService") { * @throws HttpException * @throws at.bitfire.dav4jvm.exception.DavException */ - fun queryHomeSets(client: OkHttpClient, url: HttpUrl, personal: Boolean = true) { + fun queryHomeSets(client: OkHttpClient, url: HttpUrl, accessToken: String, personal: Boolean = true) { val related = mutableSetOf() fun findRelated(root: HttpUrl, dav: Response) { @@ -223,7 +223,7 @@ class DavService: IntentService("DavService") { } } - val dav = DavResource(client, url) + val dav = DavResource(client, url, accessToken) when (service.type) { Service.TYPE_CARDDAV -> try { @@ -270,7 +270,8 @@ class DavService: IntentService("DavService") { // query related homesets (those that do not belong to the current-user-principal) for (resource in related) - queryHomeSets(client, resource, false) + queryHomeSets(client, resource, accessToken, false) + } fun saveHomesets() { @@ -309,7 +310,7 @@ class DavService: IntentService("DavService") { service.accessToken?.let { accessToken -> service.principal?.let { principalUrl -> Logger.log.fine("Querying principal $principalUrl for home sets") - queryHomeSets(httpClient, principalUrl) + queryHomeSets(httpClient, principalUrl, accessToken) } // now refresh homesets and their member collections -- GitLab From 72e6a90ac32ffd75c63f0fccc5e789ef8d4558db Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Wed, 13 Jun 2018 08:48:54 +0530 Subject: [PATCH 018/285] Use Google CalDAV API V2 instead of V1. --- .../at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt index e4246a053..f103fac79 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt @@ -333,7 +333,7 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon fun validateUrl() { model.baseUrlError.value = null try { - val uri = URI("https://www.google.com/calendar/dav/$emailAddress/events") + val uri = URI("https://apidata.googleusercontent.com/caldav/v2/$emailAddress/events") if (uri.scheme.equals("http", true) || uri.scheme.equals("https", true)) { valid = true loginModel.baseURI = uri -- GitLab From 662fea1c8f92151a3458c6e186359ed7cc577d00 Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Wed, 13 Jun 2018 10:10:31 +0530 Subject: [PATCH 019/285] Change "Create account" to "Add account", fix non-OAuth --- .../java/at/bitfire/davdroid/DavService.kt | 226 +++++++++--------- .../at/bitfire/davdroid/db/AppDatabase.kt | 4 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 116 insertions(+), 116 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/DavService.kt b/app/src/main/java/at/bitfire/davdroid/DavService.kt index 4ee9b1bba..f2445c9ef 100644 --- a/app/src/main/java/at/bitfire/davdroid/DavService.kt +++ b/app/src/main/java/at/bitfire/davdroid/DavService.kt @@ -190,7 +190,7 @@ class DavService: IntentService("DavService") { * @throws HttpException * @throws at.bitfire.dav4jvm.exception.DavException */ - fun queryHomeSets(client: OkHttpClient, url: HttpUrl, accessToken: String, personal: Boolean = true) { + fun queryHomeSets(client: OkHttpClient, url: HttpUrl, accessToken: String?, personal: Boolean = true) { val related = mutableSetOf() fun findRelated(root: HttpUrl, dav: Response) { @@ -306,139 +306,139 @@ class DavService: IntentService("DavService") { .build().use { client -> val httpClient = client.okHttpClient + val accessToken = service.accessToken + // refresh home set list (from principal) - service.accessToken?.let { accessToken -> - service.principal?.let { principalUrl -> - Logger.log.fine("Querying principal $principalUrl for home sets") - queryHomeSets(httpClient, principalUrl, accessToken) - } - // now refresh homesets and their member collections - val itHomeSets = homeSets.iterator() - while (itHomeSets.hasNext()) { - val homeSet = itHomeSets.next() - Logger.log.fine("Listing home set ${homeSet.key}") + service.principal?.let { principalUrl -> + Logger.log.fine("Querying principal $principalUrl for home sets") + queryHomeSets(httpClient, principalUrl, accessToken) + } + +// now refresh homesets and their member collections + val itHomeSets = homeSets.iterator() + while (itHomeSets.hasNext()) { + val homeSet = itHomeSets.next() + Logger.log.fine("Listing home set ${homeSet.key}") + + try { + DavResource(httpClient, homeSet.key, accessToken).propfind( + 1, + *DAV_COLLECTION_PROPERTIES + ) { response, relation -> + if (!response.isSuccess()) + return@propfind + + if (relation == Response.HrefRelation.SELF) { + // this response is about the homeset itself + homeSet.value.displayName = + response[DisplayName::class.java]?.displayName + homeSet.value.privBind = + response[CurrentUserPrivilegeSet::class.java]?.mayBind + ?: true + } + + // in any case, check whether the response is about a useable collection + val info = Collection.fromDavResponse(response) ?: return@propfind + info.serviceId = serviceId + info.confirmed = true + Logger.log.log(Level.FINE, "Found collection", info) + + // remember usable collections + if ((service.type == Service.TYPE_CARDDAV && info.type == Collection.TYPE_ADDRESSBOOK) || + (service.type == Service.TYPE_CALDAV && arrayOf( + Collection.TYPE_CALENDAR, + Collection.TYPE_WEBCAL + ).contains(info.type)) + ) + collections[response.href] = info + } + } catch (e: HttpException) { + if (e.code in arrayOf(403, 404, 410)) + // delete home set only if it was not accessible (40x) + itHomeSets.remove() + } + } + // check/refresh unconfirmed collections + val collectionsIter = collections.entries.iterator() + while (collectionsIter.hasNext()) { + val currentCollection = collectionsIter.next() + val (url, info) = currentCollection + if (!info.confirmed) try { - DavResource(httpClient, homeSet.key, accessToken).propfind( - 1, + // this collection doesn't belong to a homeset anymore, otherwise it would have been confirmed + info.homeSetId = null + + DavResource(httpClient, url).propfind( + 0, *DAV_COLLECTION_PROPERTIES - ) { response, relation -> + ) { response, _ -> if (!response.isSuccess()) return@propfind - if (relation == Response.HrefRelation.SELF) { - // this response is about the homeset itself - homeSet.value.displayName = - response[DisplayName::class.java]?.displayName - homeSet.value.privBind = - response[CurrentUserPrivilegeSet::class.java]?.mayBind - ?: true - } - - // in any case, check whether the response is about a useable collection - val info = + val collection = Collection.fromDavResponse(response) ?: return@propfind - info.serviceId = serviceId - info.confirmed = true - Logger.log.log(Level.FINE, "Found collection", info) + collection.serviceId = + info.serviceId // use same service ID as previous entry + collection.confirmed = true - // remember usable collections - if ((service.type == Service.TYPE_CARDDAV && info.type == Collection.TYPE_ADDRESSBOOK) || - (service.type == Service.TYPE_CALDAV && arrayOf( + // remove unusable collections + if ((service.type == Service.TYPE_CARDDAV && collection.type != Collection.TYPE_ADDRESSBOOK) || + (service.type == Service.TYPE_CALDAV && !arrayOf( Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL - ).contains(info.type)) + ).contains(collection.type)) || + (collection.type == Collection.TYPE_WEBCAL && collection.source == null) ) - collections[response.href] = info + collectionsIter.remove() + else + // update this collection in list + currentCollection.setValue(collection) } } catch (e: HttpException) { if (e.code in arrayOf(403, 404, 410)) - // delete home set only if it was not accessible (40x) - itHomeSets.remove() + // delete collection only if it was not accessible (40x) + collectionsIter.remove() + else + throw e } - } + } - // check/refresh unconfirmed collections - val collectionsIter = collections.entries.iterator() - while (collectionsIter.hasNext()) { - val currentCollection = collectionsIter.next() - val (url, info) = currentCollection - if (!info.confirmed) - try { - // this collection doesn't belong to a homeset anymore, otherwise it would have been confirmed - info.homeSetId = null - - DavResource(httpClient, url).propfind( - 0, - *DAV_COLLECTION_PROPERTIES - ) { response, _ -> - if (!response.isSuccess()) - return@propfind - - val collection = - Collection.fromDavResponse(response) ?: return@propfind - collection.serviceId = - info.serviceId // use same service ID as previous entry - collection.confirmed = true - - // remove unusable collections - if ((service.type == Service.TYPE_CARDDAV && collection.type != Collection.TYPE_ADDRESSBOOK) || - (service.type == Service.TYPE_CALDAV && !arrayOf( - Collection.TYPE_CALENDAR, - Collection.TYPE_WEBCAL - ).contains(collection.type)) || - (collection.type == Collection.TYPE_WEBCAL && collection.source == null) - ) - collectionsIter.remove() - else - // update this collection in list - currentCollection.setValue(collection) - } - } catch (e: HttpException) { - if (e.code in arrayOf(403, 404, 410)) - // delete collection only if it was not accessible (40x) - collectionsIter.remove() - else - throw e - } - } + // check/refresh unconfirmed collections + val itCollections = collections.entries.iterator() + while (itCollections.hasNext()) { + val (url, info) = itCollections.next() + if (!info.confirmed) + try { + DavResource(httpClient, url, accessToken).propfind( + 0, + *DAV_COLLECTION_PROPERTIES + ) { response, _ -> + if (!response.isSuccess()) + return@propfind - // check/refresh unconfirmed collections - val itCollections = collections.entries.iterator() - while (itCollections.hasNext()) { - val (url, info) = itCollections.next() - if (!info.confirmed) - try { - DavResource(httpClient, url, accessToken).propfind( - 0, - *DAV_COLLECTION_PROPERTIES - ) { response, _ -> - if (!response.isSuccess()) - return@propfind - - val collection = - Collection.fromDavResponse(response) ?: return@propfind - collection.confirmed = true - - // remove unusable collections - if ((service.type == Service.TYPE_CARDDAV && collection.type != Collection.TYPE_ADDRESSBOOK) || - (service.type == Service.TYPE_CALDAV && !arrayOf( - Collection.TYPE_CALENDAR, - Collection.TYPE_WEBCAL - ).contains(collection.type)) || - (collection.type == Collection.TYPE_WEBCAL && collection.source == null) - ) - itCollections.remove() - } - } catch (e: HttpException) { - if (e.code in arrayOf(403, 404, 410)) - // delete collection only if it was not accessible (40x) + val collection = + Collection.fromDavResponse(response) ?: return@propfind + collection.confirmed = true + + // remove unusable collections + if ((service.type == Service.TYPE_CARDDAV && collection.type != Collection.TYPE_ADDRESSBOOK) || + (service.type == Service.TYPE_CALDAV && !arrayOf( + Collection.TYPE_CALENDAR, + Collection.TYPE_WEBCAL + ).contains(collection.type)) || + (collection.type == Collection.TYPE_WEBCAL && collection.source == null) + ) itCollections.remove() - else - throw e } - } + } catch (e: HttpException) { + if (e.code in arrayOf(403, 404, 410)) + // delete collection only if it was not accessible (40x) + itCollections.remove() + else + throw e + } } } diff --git a/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt b/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt index b23e43d3f..fed434e0c 100644 --- a/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt +++ b/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt @@ -116,8 +116,8 @@ abstract class AppDatabase: RoomDatabase() { "CREATE TABLE service(" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," + "accountName TEXT NOT NULL," + - "accessToken TEXT NOT NULL," + - "refreshToken TEXT NOT NULL," + + "accessToken TEXT , " + + "refreshToken TEXT , " + "type TEXT NOT NULL," + "principal TEXT DEFAULT NULL" + ")", diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e8a29cacd..72da3cfa3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -270,7 +270,7 @@ Login with URL and client certificate Select certificate Login - Create account + Add account Account name Use of apostrophes (\'), have been reported to cause problems on some devices. Use your email address as account name because Android will use the account name as ORGANIZER field for events you create. You can\'t have two accounts with the same name. -- GitLab From acc3ca1b4c8b5850b3898ffed8539ae382e640c1 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 23 Aug 2022 11:35:44 +0200 Subject: [PATCH 020/285] Version bump to 4.2.3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4308cfb06..655b1c88a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { defaultConfig { applicationId "at.bitfire.davdroid" - versionCode 402020000 - versionName '4.2.2' + versionCode 402030001 + versionName '4.2.3' buildConfigField "long", "buildTime", System.currentTimeMillis() + "L" setProperty "archivesBaseName", "davx5-ose-" + getVersionName() -- GitLab From 62912dc83aa9ee4bc2bf275a35d776b03474394a Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 23 Aug 2022 12:01:17 +0200 Subject: [PATCH 021/285] Add F-Droid changelog --- fastlane/metadata/android/en-US/changelogs/402030001.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/402030001.txt diff --git a/fastlane/metadata/android/en-US/changelogs/402030001.txt b/fastlane/metadata/android/en-US/changelogs/402030001.txt new file mode 100644 index 000000000..ac0293854 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/402030001.txt @@ -0,0 +1,3 @@ +* CalDAV: refactor and improve event validation and repairing (especially with recurring events) +* WebDAV: connections now have a delete symbol, so that people know that they would delete the connection +* minor bug fixes and improvements -- GitLab From f6b230f68d9c5a240a3e62f5f70e0620d48d5083 Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Thu, 14 Jun 2018 12:26:31 +0530 Subject: [PATCH 022/285] Use AuthState instead of accessToken and refreshToken. --- .../java/at/bitfire/davdroid/DavService.kt | 4 ++- .../at/bitfire/davdroid/db/AppDatabase.kt | 5 ++-- .../at/bitfire/davdroid/db/Credentials.kt | 5 ++-- .../java/at/bitfire/davdroid/db/Service.kt | 5 ++-- .../ui/setup/AccountDetailsFragment.kt | 8 +++--- .../davdroid/ui/setup/DavResourceFinder.kt | 6 ++-- .../setup/DefaultLoginCredentialsFragment.kt | 2 +- .../ui/setup/GoogleAuthenticatorFragment.kt | 28 ++++++++----------- 8 files changed, 29 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/DavService.kt b/app/src/main/java/at/bitfire/davdroid/DavService.kt index f2445c9ef..85a84244d 100644 --- a/app/src/main/java/at/bitfire/davdroid/DavService.kt +++ b/app/src/main/java/at/bitfire/davdroid/DavService.kt @@ -29,6 +29,7 @@ import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.ui.DebugInfoActivity import at.bitfire.davdroid.ui.NotificationUtils import dagger.hilt.android.AndroidEntryPoint +import net.openid.appauth.AuthState import okhttp3.HttpUrl import okhttp3.OkHttpClient import java.lang.ref.WeakReference @@ -306,7 +307,8 @@ class DavService: IntentService("DavService") { .build().use { client -> val httpClient = client.okHttpClient - val accessToken = service.accessToken + val authState = service.authState + val accessToken = if (authState != null) AuthState.jsonDeserialize(authState).accessToken else null // refresh home set list (from principal) diff --git a/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt b/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt index fed434e0c..6cc264093 100644 --- a/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt +++ b/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt @@ -116,13 +116,12 @@ abstract class AppDatabase: RoomDatabase() { "CREATE TABLE service(" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," + "accountName TEXT NOT NULL," + - "accessToken TEXT , " + - "refreshToken TEXT , " + + "authState TEXT ," + "type TEXT NOT NULL," + "principal TEXT DEFAULT NULL" + ")", "CREATE UNIQUE INDEX index_service_accountName_type ON service(accountName, type)", - "INSERT INTO service(id, accountName, accessToken, refreshToken, type, principal) SELECT _id, accountName, accessToken, refreshToken, service, principal FROM services", + "INSERT INTO service(id, accountName, authState, type, principal) SELECT _id, accountName, authState, service, principal FROM services", "DROP TABLE services", // migrate "homesets" to "homeset": rename columns, make id NOT NULL diff --git a/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt b/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt index cc80bf405..d4ab1812c 100644 --- a/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt +++ b/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt @@ -4,11 +4,12 @@ package at.bitfire.davdroid.db +import net.openid.appauth.AuthState + data class Credentials( val userName: String? = null, val password: String? = null, - val accessToken: String? = null, - val refreshToken: String? = null, + val authState: AuthState? = null, val certificateAlias: String? = null ) { diff --git a/app/src/main/java/at/bitfire/davdroid/db/Service.kt b/app/src/main/java/at/bitfire/davdroid/db/Service.kt index 8a4a823ff..066bb234b 100644 --- a/app/src/main/java/at/bitfire/davdroid/db/Service.kt +++ b/app/src/main/java/at/bitfire/davdroid/db/Service.kt @@ -20,9 +20,8 @@ data class Service( var accountName: String, - var accessToken: String?, - var refreshToken: String?, - + var authState: String?, + var type: String, var principal: HttpUrl? diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 279934af5..f00e0e444 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -180,7 +180,7 @@ class AccountDetailsFragment : Fragment() { val addrBookAuthority = context.getString(R.string.address_books_authority) if (config.cardDAV != null) { // insert CardDAV service - val id = insertService(name, credentials?.accessToken, credentials?.refreshToken, Service.TYPE_CARDDAV, config.cardDAV) + val id = insertService(name, credentials?.authState?.jsonSerializeString(), Service.TYPE_CARDDAV, config.cardDAV) // initial CardDAV account settings accountSettings.setGroupMethod(groupMethod) @@ -197,7 +197,7 @@ class AccountDetailsFragment : Fragment() { if (config.calDAV != null) { // insert CalDAV service - val id = insertService(name, credentials?.accessToken, credentials?.refreshToken, Service.TYPE_CALDAV, config.calDAV) + val id = insertService(name, credentials?.authState?.jsonSerializeString(), Service.TYPE_CALDAV, config.calDAV) // start CalDAV service detection (refresh collections) refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id) @@ -226,9 +226,9 @@ class AccountDetailsFragment : Fragment() { return result } - private fun insertService(accountName: String, accessToken: String?, refreshToken: String?, type: String, info: DavResourceFinder.Configuration.ServiceInfo): Long { + private fun insertService(accountName: String, authState: String?, type: String, info: DavResourceFinder.Configuration.ServiceInfo): Long { // insert service - val service = Service(0, accountName, accessToken, refreshToken, type, info.principal) + val service = Service(0, accountName, authState, type, info.principal) val serviceId = db.serviceDao().insertOrReplace(service) // insert home sets diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/DavResourceFinder.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/DavResourceFinder.kt index cd346e432..7b8ce4121 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/DavResourceFinder.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/DavResourceFinder.kt @@ -163,7 +163,7 @@ class DavResourceFinder( private fun checkUserGivenURL(baseURL: HttpUrl, service: Service, config: Configuration.ServiceInfo) { log.info("Checking user-given URL: $baseURL") - val davBase = DavResource(httpClient.okHttpClient, baseURL, loginModel.credentials!!.accessToken, log) + val davBase = DavResource(httpClient.okHttpClient, baseURL, loginModel.credentials!!.authState!!.accessToken, log) try { when (service) { Service.CARDDAV -> { @@ -313,7 +313,7 @@ class DavResourceFinder( fun providesService(url: HttpUrl, service: Service): Boolean { var provided = false try { - DavResource(httpClient.okHttpClient, url, loginModel.credentials!!.accessToken, log).options { capabilities, _ -> + DavResource(httpClient.okHttpClient, url, loginModel.credentials!!.authState!!.accessToken, log).options { capabilities, _ -> if ((service == Service.CARDDAV && capabilities.contains("addressbook")) || (service == Service.CALDAV && capabilities.contains("calendar-access"))) provided = true @@ -401,7 +401,7 @@ class DavResourceFinder( @Throws(IOException::class, HttpException::class, DavException::class) fun getCurrentUserPrincipal(url: HttpUrl, service: Service?): HttpUrl? { var principal: HttpUrl? = null - DavResource(httpClient.okHttpClient, url, loginModel.credentials!!.accessToken, log).propfind(0, CurrentUserPrincipal.NAME) { response, _ -> + DavResource(httpClient.okHttpClient, url, loginModel.credentials!!.authState!!.accessToken, log).propfind(0, CurrentUserPrincipal.NAME) { response, _ -> response[CurrentUserPrincipal::class.java]?.href?.let { href -> response.requestedUrl.resolve(href)?.let { log.info("Found current-user-principal: $it") diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsFragment.kt index 445079025..24f939cf8 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsFragment.kt @@ -179,7 +179,7 @@ class DefaultLoginCredentialsFragment : Fragment() { loginModel.credentials = when { // username/password and client certificate model.loginUseUsernamePassword.value == true && model.loginUseClientCertificate.value == true -> - Credentials(username, password, alias) + Credentials(username, password, null, alias) // user/name password only model.loginUseUsernamePassword.value == true && model.loginUseClientCertificate.value == false -> diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt index f103fac79..66eb63979 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt @@ -305,8 +305,8 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon emailAddress = userInfoJson!!.getString("email") } - if (validate(emailAddress, authState!!.accessToken!!, authState!!.refreshToken!!)) - requireFragmentManager().beginTransaction() + if (validate(emailAddress, authState!!)) + requireFragmentManager().beginTransaction() .replace(android.R.id.content, DetectConfigurationFragment(), null) .addToBackStack(null) .commit() @@ -327,7 +327,7 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon } - private fun validate(emailAddress: String, accessToken: String, refreshToken: String): Boolean { + private fun validate(emailAddress: String, authState: AuthState): Boolean { var valid = false fun validateUrl() { @@ -344,20 +344,14 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon } } - when { - - model.loginWithUrlAndTokens.value == true -> { - validateUrl() - - model.usernameError.value = null - - if (loginModel.baseURI != null) { - valid = true - loginModel.credentials = Credentials(emailAddress, null, accessToken, refreshToken, null) - } - } - - } + if(model.loginWithUrlAndTokens.value == true) { + validateUrl() + model.usernameError.value = null + if (loginModel.baseURI != null) { + valid = true + loginModel.credentials = Credentials(emailAddress, null, authState, null) + } + } return valid } -- GitLab From ed24f57527efef3db993b36106932e28a57a97aa Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Fri, 15 Jun 2018 19:25:45 +0530 Subject: [PATCH 023/285] Implemented OAuth token refresh. --- .../davdroid/settings/AccountSettings.kt | 49 +++++++++++++------ .../CalendarsSyncAdapterService.kt | 40 ++++++++++++++- .../syncadapter/ContactsSyncAdapterService.kt | 40 ++++++++++++++- .../syncadapter/TasksSyncAdapterService.kt | 42 +++++++++++++++- 4 files changed, 152 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt index f030ab022..3ab7e55d4 100644 --- a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt @@ -47,6 +47,7 @@ import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.property.Url +import net.openid.appauth.AuthState import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.apache.commons.lang3.StringUtils import org.dmfs.tasks.contract.TaskContract @@ -89,8 +90,7 @@ class AccountSettings( const val KEY_SYNC_INTERVAL_TASKS = "sync_interval_tasks" const val KEY_USERNAME = "user_name" - const val KEY_ACCESS_TOKEN = "access_token" - const val KEY_REFRESH_TOKEN = "refresh_token" + const val KEY_AUTH_STATE = "auth_state" const val KEY_CERTIFICATE_ALIAS = "certificate_alias" const val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false) @@ -145,10 +145,8 @@ class AccountSettings( bundle.putString(KEY_USERNAME, credentials.userName) if (credentials.certificateAlias != null) bundle.putString(KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) - if (credentials.accessToken != null) - bundle.putString(KEY_ACCESS_TOKEN, credentials.accessToken) - if (credentials.refreshToken != null) - bundle.putString(KEY_REFRESH_TOKEN, credentials.refreshToken) + if (credentials.authState != null) + bundle.putString(KEY_AUTH_STATE, credentials.authState.jsonSerializeString()) } return bundle @@ -235,18 +233,37 @@ class AccountSettings( // authentication settings - fun credentials() = Credentials( - accountManager.getUserData(account, KEY_USERNAME), - accountManager.getPassword(account), - accountManager.getUserData(account, KEY_ACCESS_TOKEN), - accountManager.getUserData(account, KEY_REFRESH_TOKEN), - accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS) - ) + fun credentials(): Credentials { + return if (accountManager.getUserData(account, KEY_AUTH_STATE).isNullOrEmpty()) { + Credentials( + accountManager.getUserData(account, KEY_USERNAME), + accountManager.getPassword(account), + null, + accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS) + ) + } else { + Credentials( + accountManager.getUserData(account, KEY_USERNAME), + accountManager.getPassword(account), + AuthState.jsonDeserialize(accountManager.getUserData(account, KEY_AUTH_STATE)), + accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS) + ) + } + } fun credentials(credentials: Credentials) { - accountManager.setUserData(account, KEY_USERNAME, credentials.userName) - accountManager.setPassword(account, credentials.password) - accountManager.setUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) + if (credentials.authState == null) { + accountManager.setUserData(account, KEY_USERNAME, credentials.userName) + accountManager.setPassword(account, credentials.password) + accountManager.setUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) + } + else { + accountManager.setUserData(account, KEY_USERNAME, credentials.userName) + accountManager.setPassword(account, credentials.password) + accountManager.setUserData(account, KEY_AUTH_STATE, credentials.authState.jsonSerializeString()) + accountManager.setUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) + } + } diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt index 83633b5ba..67b16cb8c 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt @@ -9,15 +9,18 @@ import android.content.ContentResolver import android.content.Context import android.content.SyncResult import android.os.Bundle +import android.os.AsyncTask import android.provider.CalendarContract import at.bitfire.davdroid.HttpClient import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalCalendar import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.ical4android.AndroidCalendar +import net.openid.appauth.AuthorizationService import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import java.util.logging.Level @@ -60,7 +63,42 @@ class CalendarsSyncAdapterService: SyncAdapterService() { for (calendar in calendars) { Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}") CalendarSyncManager(context, account, accountSettings, extras, httpClient.value, authority, syncResult, calendar).let { - it.performSync() + val authState = accountSettings.credentials().authState + if (authState != null) { + if (authState.needsTokenRefresh) { + val tokenRequest = authState.createTokenRefreshRequest() + + AuthorizationService(context).performTokenRequest(tokenRequest) { tokenResponse, ex -> + authState.update(tokenResponse, ex) + accountSettings.credentials( + Credentials( + account.name, + null, + authState, + null + ) + ) + it.accountSettings.credentials( + Credentials( + it.account.name, + null, + authState, + null + ) + ) + object : AsyncTask() { + override fun doInBackground(vararg params: Void): Void? { + it.performSync() + return null + } + }.execute() + } + } else { + it.performSync() + } + } else { + it.performSync() + } } } } catch(e: Exception) { diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt index 454d06535..5de00c3d1 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt @@ -9,13 +9,16 @@ import android.content.ContentProviderClient import android.content.ContentResolver import android.content.Context import android.content.SyncResult +import android.os.AsyncTask import android.os.Bundle import android.provider.ContactsContract import at.bitfire.davdroid.HttpClient import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.settings.AccountSettings +import net.openid.appauth.AuthorizationService import java.util.logging.Level class ContactsSyncAdapterService: SyncAdapterService() { @@ -64,7 +67,42 @@ class ContactsSyncAdapterService: SyncAdapterService() { Logger.log.info("Taking settings from: ${addressBook.mainAccount}") ContactsSyncManager(context, account, accountSettings, httpClient.value, extras, authority, syncResult, provider, addressBook).let { - it.performSync() + val authState = accountSettings.credentials().authState + if (authState != null) { + if (authState.needsTokenRefresh) { + val tokenRequest = authState.createTokenRefreshRequest() + + AuthorizationService(context).performTokenRequest(tokenRequest) { tokenResponse, ex -> + authState.update(tokenResponse, ex) + accountSettings.credentials( + Credentials( + account.name, + null, + authState, + null + ) + ) + it.accountSettings.credentials( + Credentials( + it.account.name, + null, + authState, + null + ) + ) + object : AsyncTask() { + override fun doInBackground(vararg params: Void): Void? { + it.performSync() + return null + } + }.execute() + } + } else { + it.performSync() + } + } else { + it.performSync() + } } } catch(e: Exception) { Logger.log.log(Level.SEVERE, "Couldn't sync contacts", e) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt index 8b03e8035..c61070f8d 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt @@ -20,6 +20,11 @@ import at.bitfire.davdroid.resource.LocalTaskList import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.ical4android.AndroidTaskList import at.bitfire.ical4android.TaskProvider +import android.os.AsyncTask +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import at.bitfire.davdroid.db.Credentials +import net.openid.appauth.AuthorizationService import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import org.dmfs.tasks.contract.TaskContract @@ -70,7 +75,42 @@ open class TasksSyncAdapterService: SyncAdapterService() { for (taskList in taskLists) { Logger.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]") TasksSyncManager(context, account, accountSettings, httpClient.value, extras, authority, syncResult, taskList).let { - it.performSync() + val authState = accountSettings.credentials().authState + if (authState != null) { + if (authState.needsTokenRefresh) { + val tokenRequest = authState.createTokenRefreshRequest() + + AuthorizationService(context).performTokenRequest(tokenRequest) { tokenResponse, ex -> + authState.update(tokenResponse, ex) + accountSettings.credentials( + Credentials( + account.name, + null, + authState, + null + ) + ) + it.accountSettings.credentials( + Credentials( + it.account.name, + null, + authState, + null + ) + ) + object : AsyncTask() { + override fun doInBackground(vararg params: Void): Void? { + it.performSync() + return null + } + }.execute() + } + } else { + it.performSync() + } + } else { + it.performSync() + } } } } catch (e: TaskProvider.ProviderTooOldException) { -- GitLab From ff695d7f134b709590f3ebebff7b2f89459c0645 Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Sat, 16 Jun 2018 10:44:08 +0530 Subject: [PATCH 024/285] Translated modified strings to all languages. --- app/src/main/res/values-ca/strings.xml | 6 +++--- app/src/main/res/values-cs/strings.xml | 6 +++--- app/src/main/res/values-da/strings.xml | 6 +++--- app/src/main/res/values-de/strings.xml | 4 ++-- app/src/main/res/values-es/strings.xml | 6 +++--- app/src/main/res/values-fr/strings.xml | 6 +++--- app/src/main/res/values-hu/strings.xml | 6 +++--- app/src/main/res/values-it/strings.xml | 6 +++--- app/src/main/res/values-ja/strings.xml | 6 +++--- app/src/main/res/values-nb-rNO/strings.xml | 6 +++--- app/src/main/res/values-nl/strings.xml | 6 +++--- app/src/main/res/values-pl/strings.xml | 6 +++--- app/src/main/res/values-pt/strings.xml | 4 ++-- app/src/main/res/values-ru/strings.xml | 5 ++--- app/src/main/res/values-sr/strings.xml | 4 ++-- app/src/main/res/values-tr/strings.xml | 4 ++-- app/src/main/res/values-uk/strings.xml | 6 +++--- app/src/main/res/values-zh-rTW/strings.xml | 6 +++--- app/src/main/res/values/email_providers_auth_config.xml | 2 +- 19 files changed, 50 insertions(+), 51 deletions(-) diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 32453fd23..166f7c9cc 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -1,9 +1,9 @@ - DAVx⁵ + Responsable de comptes El compte no existeix (eliminat) - Llibreta d’adreces del DAVx⁵ + Llibreta d’adreces del AccountManager Llibreta d’adreces Cal aquest camp Ajuda @@ -235,7 +235,7 @@ Inici de sessió amb un URL i un certificat de client Selecciona el certificat Inici de sessió - Crea un compte + Afegir compte Nom del compte S\'ha informat que l\'ús d\'apòstrofs (\'), causa problemes en alguns dispositius. Utilitzeu la vostra adreça de correu electrònic com a nom del compte perquè l\'Android utilitzarà el nom del compte com a camp ORGANITZADOR per als esdeveniments que creeu. No poden haver-hi dos comptes amb el mateix nom. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 63d4fe579..886d10217 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -1,9 +1,9 @@ - DAVx⁵ + Správce účtu Účet (už) neexistuje - DAVx⁵ adresář kontaktů + Správce účtu adresář kontaktů Adresáře kontaktů Tuto kolonku je třeba vyplnit Pomoc @@ -234,7 +234,7 @@ Přihlásit pomocí URL a klientského certifikátu Vybrat certifikát Přihlášení - Vytvořit účet + Přidat účet Název účtu Použití apostrofů (\') bylo u některých zařízení hlášeno jako problematické. Pro jméno účtu použijte svou e-mailovou adresu, protože Android bude brát jméno účtu jako údaj pro ORGANIZÁTORA vytvořených událostí. Nelze mít dva účty stejného jména. diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 8187429f9..db3c7e494 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -1,9 +1,9 @@ - DAVx⁵ + Kundechef Konto findes ikke (længere) - DAVx⁵ adressebog + Kundechef adressebog Adressebøger Feltet er påkrævet Hjælp @@ -235,7 +235,7 @@ Log ind med URL og klientcertifikat Vælge certifikat Log ind - Oprette konto + Tilføj konto Kontonavn Du kan ikke bruge anførelsestegn (\') på alle mobiler. Brug e-mail adresse som kontonavn da Android bruger kontonavn til ORGANIZER-felt for oprettede aktiviteter. Man kan ikke have to konti med samme navn. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d13ada330..daf7efc7d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,7 +1,7 @@ - DAVx⁵ + Buchhalter Konto nicht (mehr) vorhanden DAVx⁵-Adressbuch Adressbücher @@ -235,7 +235,7 @@ Mit URL und Client-Zertifikat anmelden Zertifikat auswählen Anmelden - Konto anlegen + hinzufügen anlegen Kontoname Auf manchen Geräten führt die Verwendung von einfachen Anführungszeichen (\') zu Problemen. Verwenden Sie Ihre E-Mail-Adresse als Kontonamen, da Android den Kontonamen als ORGANIZER einsetzt. Es kann allerdings keine zwei Konten mit dem gleichen Namen geben. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 3dc8a1c11..c3082eca4 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,9 +1,9 @@ - DAVx⁵ + Gerente de cuentas La cuenta (ya) no existe - Agenda DAVx⁵ + Agenda Gerente de cuentas Agendas Este campo es requerido Ayuda @@ -235,7 +235,7 @@ Iniciar sesión con URL y certificado del cliente Seleccionar un certificado Registrar - Crear cuenta + Añadir cuenta Nombre de cuenta Se han reportado problemas con el uso de apóstrofes(\') en algunos dispositivos. Usa tu dirección de correo como nombre de cuenta puesto que Android usará el nombre de la cuenta como campo de \"organizador\" en los eventos que cree. No puedes tener dos cuentas con el mismo nombre. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 596f94040..46f1ad789 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,9 +1,9 @@ - DAVx⁵ + Gestionnaire de compte Le compte n’existe plus (désormais) - Carnet d\'adresses DAVx⁵ + Carnet d\'adresses Gestionnaire de compte Carnets d\'adresses Ce champ est requis Aide @@ -222,7 +222,7 @@ Se connecter avec l\'URL et le certificat client Choisir le certificat Se connecter - Créer un compte + Ajouter un compte Nom du compte L\'utilisation d\'apostrophes (\') est connue pour causer des problèmes sur certains appareils. Utilisez votre adresse e-mail comme nom de compte car Android utilisera ce nom en tant que champ ORGANISATEUR pour les événements que vous créerez. Vous ne pouvez pas avoir deux comptes avec le même nom. diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 378c665c4..ed61fb71c 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -1,9 +1,9 @@ - DAVx⁵ + Fiókkezelő A felhasználói fiók (már) nem létezik - DAVx⁵ címjegyzék + Fiókkezelő címjegyzék Címjegyzékek Ennek e mezőnek a megadása kötelező Súgó @@ -235,7 +235,7 @@ Bejelentkezés URL és tanúsítvány segítségével Tanúsítvány kiválasztása Bejelentkezés - Fiók létrehozása + Fiók hozzáadása A fiók neve Az aposztróf (\') használata a visszajelzések szerinte egyes eszközökön problémát okoz. Használja az email címet fióknévként, mert később a létrehozandó események szervezőjeként (ORGANIZER mező) az Android ezt fogja használni. Két fiókot nem lehet azonos néven létrehozni. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 67d536005..bd7dc4b96 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,9 +1,9 @@ - DAVx⁵ + Account Manager Account inesistente (o cancellato) - Rubrica DAVx⁵ + Rubrica Account Manager Rubriche Questo campo è necessario Aiuto @@ -211,7 +211,7 @@ Accedi con URL e certificato client Seleziona certificato Login - Crea account + Aggiungi account Nome account L\'utilizzo di apostrofi (\') potrebbe causare problemi su alcuni dispositivi. Inserisci il tuo indirizzo email come nome dell\'account in quanto Android userà il nome dell\'account nel campo ORGANIZER degli eventi creati. Non è possibile avere due account con nome uguale. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 09c31e81e..dcc4274c6 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1,9 +1,9 @@ - DAVx⁵ + アカウントマネージャー アカウントがありません - DAVx⁵ アドレス帳 + アカウントマネージャー アドレス帳 アドレス帳 ヘルプ アカウントの管理 @@ -118,7 +118,7 @@ URL とクライアント証明書でログイン 証明書を選択 ログイン - アカウントを作成 + アカウントを追加する アカウント名 Android はあなたが作成した予定の ORGANIZER フィールドとしてアカウント名を使用するので、アカウント名としてメールアドレスを使用してください。同じ名前のアカウントを 2 つ持つことはできません。 連絡先グループ方法: diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index ce2fe9117..f00f7a73d 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -1,8 +1,8 @@ - DAVx⁵ - DAVx⁵-adressebok + Account Manager + Account Manager-adressebok Adressebøker Hjelp Behandle kontoer @@ -111,7 +111,7 @@ Brukernavn påkrevd Landings-nettadresse Logg inn - Opprett konto + Legg til konto Kontonavn Bruk din e-postadresse som kontonavn fordi Android vil bruke kontonavnet som ORGANISATOR-felt for hendelser du oppretter. Du kan ikke ha to kontoer med samme navn. Kontaktgruppemetode: diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 2714f0271..4b89e90e5 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1,9 +1,9 @@ - DAVx⁵ + Account Manager Account bestaat niet (of niet meer) - DAVx⁵ Adresboek + Account Manager Adresboek Adresboeken Dit veld is verplicht Hulp @@ -235,7 +235,7 @@ Login met URL en client certificaat Certificaat selecteren Login - Account aanmaken + Account toevoegen Accountnaam Het gebruik van apostrofs (\') heeft op sommige apparaten problemen veroorzaakt. Gebruik het eigen e-mailadres als accountnaam, want Android gebruikt het als ORGANIZER veld voor gebeurtenissen. Twee accounts met hetzelfde adres kan niet. diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index de5e86fed..825fedafc 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1,9 +1,9 @@ - DAVx⁵ + Menadżer konta Konto (już) nie istnieje - Książka adresowa DAVx⁵ + Książka adresowa Menadżer konta Książka adresowa To pole jest wymagane Pomoc @@ -235,7 +235,7 @@ Logowanie za pomocą adresu URL i certyfikatu klienta Wybierz certyfikat Zaloguj - Stwórz konto + Dodaj konto Nazwa konta Zgłoszono, że użycie apostrofów (\') powoduje problemy na niektórych urządzeniach. Użyj swojego adresu e‑mail jako nazwy konta, ponieważ Android będzie używał nazwy konta jako pola ORGANIZATOR dla wydarzeń, które stworzysz. Nie możesz posiadać dwóch kont o takiej samej nazwie. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 8b66834eb..fcc0eead6 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,7 +1,7 @@ - DAVx⁵ + Gerente de contas A conta não existe mais Livro de endereços DAVx⁵ Livros de endereços @@ -180,7 +180,7 @@ Autenticação com URL e certificado do cliente Selecionar certificado Autenticar - Criar conta + Adicionar Conta Nome da conta Use seu endereço de e-mail como nome da conta porque o Android irá usar esse nome como campo AGENDA nos eventos que você criar. Não é possível ter duas contas com o mesmo nome. Método do grupo Contato: diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 6940f3b01..5d2509bdc 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,8 +1,7 @@ - DAVx⁵ - Аккаунт не существует (больше) + Менеджер по работе с клиентами Адресная книга DAVx⁵ Адресные книги Это поле является обязательным @@ -235,7 +234,7 @@ Войти с URL и сертификатом клиента Выберите сертификат Войти - Создать аккаунт + Добавить аккаунт Название аккаунта По имеющимся данным, использование апострофов (\') вызывает проблемы на некоторых устройствах. Укажите ваш адрес email в качестве названия аккаунта, поскольку Android будет его использовать в поле ORGANIZER для создаваемых событий. У вас не может быть двух аккаунтов с тем же именем. diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index b47339b34..d9b60fafd 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -1,7 +1,7 @@ - ДАВдроид + Менаџер налога ДАВдроид адресар Адресари Помоћ @@ -100,7 +100,7 @@ Пријавите се УРЛ-ом и сертификатом клијента Изабери сертификат Пријава - Направи налог + Додај налог Назив налога Користите вашу е-адресу за назив налога јер Андроид користи назив налога за поље ОРГАНИЗАТОР за догађаје које направите. Не можете имати два налога истог назива. Режим група контаката: diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1e038bc4b..4d5ca99e3 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,7 +1,7 @@ - DAVx⁵ + Muhasebe Müdürü Rehberler Yardım Hesapları yönet @@ -71,7 +71,7 @@ Kullanıcı adı zorunludur Baz URL Giriş - Hesap yarat + Hesap eklemek Hesap adı Hesap ismi olarak e-posta adresini kullan çünkü Android hesap ismini yarattığın olaylarda DÜZENLEYEN alanında kullanacaktır. Aynı isimde iki faklı hesabın olamaz. Hesap adı zorunludur diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 0f0cf8621..24b28fdc0 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1,9 +1,9 @@ - DAVx⁵ + Менеджер рахунків Обліківки не існує (більше) - Адресна книга DAVx⁵ + Адресна книга Менеджер рахунків Адресні книги Це поле обовʼязкове Допомога @@ -161,7 +161,7 @@ Вхід по посиланню та сертифікату клієнта Обрати сертифікат Увійти - Створити запис + Додати обліковий Назва запису Використовуйте вашу електронну адресу як ім\'я облікового запису, так як Android буде використовувати ім\'я облікового запису в полі ORGANIZER для подій, які ви створюватимете. Ви не можете мати два облікових записи з однаковими іменами. Метод групування контактів: diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index b321324a0..007928f4a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1,9 +1,9 @@ - DAVx⁵ + 客户经理 賬戶不存在 (anymore) - DAVx⁵ 通訊錄 + 客户经理 通訊錄 通訊錄 幫助 管理帳號 @@ -131,7 +131,7 @@ 用網址和客戶端鑒權登入 點選憑證 登入 - 新建帳號 + 新增帐户 帳號名稱 使用 Email 地址當作裝置上的帳號顯示名稱,因為當您在行事曆創建活動時,Android 會把帳號顯示名稱放到「活動發起人」欄位。兩個帳號不能有相同的名稱。 聯絡人群組的儲存格式 diff --git a/app/src/main/res/values/email_providers_auth_config.xml b/app/src/main/res/values/email_providers_auth_config.xml index f4f123158..22a32a324 100644 --- a/app/src/main/res/values/email_providers_auth_config.xml +++ b/app/src/main/res/values/email_providers_auth_config.xml @@ -2,7 +2,7 @@ - Google + Google 100496780587-pbiu5eudcjm6cge2phduc6mt8mgbsmsr.apps.googleusercontent.com -- GitLab From f38e7b2dab838fd0fb18b308809a2244b72d133e Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 23 Aug 2022 21:43:36 +0200 Subject: [PATCH 025/285] Update hilt and ical4android (ical4android had dependency that caused JSONObject problem in release builds) --- build.gradle | 2 +- ical4android | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index fb131ffaa..8c44499d9 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { aboutLibraries: '8.9.4', appIntro: '6.2.0', dav4jvm: 'c61e4b0c80a5a8de1df99b4997445bb323d3ea3d', - hilt: '2.42', + hilt: '2.43.2', kotlin: '1.7.0', okhttp: '4.10.0', // latest Apache Commons versions that don't require Java 8 (Android 7) diff --git a/ical4android b/ical4android index ccea6cbd4..ce6d45486 160000 --- a/ical4android +++ b/ical4android @@ -1 +1 @@ -Subproject commit ccea6cbd487b0872d7e399f290fd00ffbb08f37f +Subproject commit ce6d454862a69c7078d2d4c419bf68da63f5ad25 -- GitLab From 7144b963e40651c5424cd5224d3baf8431aada22 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 23 Aug 2022 21:44:27 +0200 Subject: [PATCH 026/285] Version bump to 4.2.3.1-rc1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 655b1c88a..dbe56e431 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { defaultConfig { applicationId "at.bitfire.davdroid" - versionCode 402030001 - versionName '4.2.3' + versionCode 402030100 + versionName '4.2.3.1-rc.1' buildConfigField "long", "buildTime", System.currentTimeMillis() + "L" setProperty "archivesBaseName", "davx5-ose-" + getVersionName() -- GitLab From 638069904c12047f606782585a94d8d7b2b2d57e Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Sat, 16 Jun 2018 11:45:47 +0530 Subject: [PATCH 027/285] Changed string 'DAVdroid' to 'Account Manager' everywhere. --- app/src/main/res/values-cs/strings.xml | 12 ++++++------ app/src/main/res/values-da/strings.xml | 12 ++++++------ app/src/main/res/values-de/strings.xml | 12 ++++++------ app/src/main/res/values-es/strings.xml | 12 ++++++------ app/src/main/res/values-fr/strings.xml | 12 ++++++------ app/src/main/res/values-hu/strings.xml | 11 +++++------ app/src/main/res/values-it/strings.xml | 12 ++++++------ app/src/main/res/values-ja/strings.xml | 10 ++++++---- app/src/main/res/values-nb-rNO/strings.xml | 14 ++++++++------ app/src/main/res/values-nl/strings.xml | 10 +++++----- app/src/main/res/values-pl/strings.xml | 12 ++++++------ app/src/main/res/values-ru/strings.xml | 16 ++++++++-------- app/src/main/res/values-tr/strings.xml | 6 ++++-- app/src/main/res/values-uk/strings.xml | 10 ++++++---- app/src/main/res/values-zh-rTW/strings.xml | 10 ++++++---- app/src/main/res/values/strings.xml | 12 ++++++------ 16 files changed, 96 insertions(+), 87 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 886d10217..b4d7a12ca 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -122,7 +122,7 @@ Ochrana soukromí Bez připojení k Internetu. Systém Android synchronizaci nespustí. Zbývá málo místa na úložišti. Systém Android nespustí synchronizaci. - Vítejte v aplikaci DAVx⁵!\n\nNyní můžete přidat CalDAV/CardDAV účet. + Vítejte v aplikaci Správce účtu!\n\nNyní můžete přidat CalDAV/CardDAV účet. Automatická synchronizace v rámci celého systému je vypnutá Zapnout Synchronizovat všechny účty @@ -308,8 +308,8 @@ Nejsou vytvořené žádné výchozí připomínky Pokud mají být pro události bez připomínky vytvořeny výchozí připomínky: požadovaný počet minut před událostí. Pokud výchozí připomínky nechcete, nevyplňujte. Spravovat barvy kalendářů - Barvy kalendáře jsou při každé synchronizaci resetovány - Barvy kalendáře je možné nastavovat ostatními aplikacemi + Barvy kalendářů spravuje Správce účtu + Barvy kalendářů nespravuje Správce účtu Podpora pro barvy událostí Barvy událostí jsou synchronizovány Barvy událostí nejsou synchronizovány @@ -402,7 +402,7 @@ Nahrává se WebDAV soubor WebDAV připojení - DAVx⁵ oprávnění + Správce účtu oprávnění Vyžadována dodatečná oprávnění Příliš stará verze %s Nejnižší požadovaná verze: %1$s @@ -417,6 +417,6 @@ Ze serveru obdržen neplatný úkol Ignoruje se jeden či více neplatný prostředků - DAVx⁵: Zabezpečení připojení - DAVx⁵ nalezlo neznámý certifikát. Chcete mu důvěřovat? + Správce účtu: Zabezpečení připojení + Správce účtu nalezl neznámý certifikát. Chcete mu důvěřovat? diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index db3c7e494..7432d0463 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -123,7 +123,7 @@ Privatlivs politik Ingen internetforbindelse. Android kører ikke synkronisering. Lagerplds lav. Android kører ikke synkronisering. - Velkommen til DAVx⁵!\n\nDu kan nu tilføje en CalDAV/CardDAV konto. + Velkommen til Kundechef!\n\nDu kan nu tilføje en CalDAV/CardDAV konto. Automatisk synkronisering på tværs af systemet er deaktiveret Aktivere Synkronisere alle konti @@ -305,8 +305,8 @@ Ingen standard påmindelse oprettet Hvis standard påmindelse skal oprettes for hændelse uden påmindelse: antal minutter før hændelse. Efterlad tom for at deaktivere standard påmindelse. Administrer farver for kalender - Kalender farver nulstilles ved hver synkronisering - Kalender farver kan sættes fra andre programmer + Kalenderfarver administreres af Kundechef + Kalenderfarver sættes ikke fra Kundechef Farver for begivenheder Farver for begivenheder er synkroniseret Farver for begivenheder er ikke synkroniseret @@ -399,7 +399,7 @@ Overfør WebDAV fil Montere WebDAV - DAVx⁵-rettigheder + Kundechef-rettigheder Yderligere adgang påkrævet %s for gammel Påkrævet version: %1$s @@ -414,6 +414,6 @@ Modtaget ugyldig opgave fra server Ignorere en eller flere ugyldige kilder - DAVx⁵: Forbindelsessikkerhed - DAVx⁵ er stødt på et ukendt certifikat. Vil du stole på det? + Kundechef: Forbindelsessikkerhed + Kundechef er stødt på et ukendt certifikat. Vil du stole på det? diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index daf7efc7d..1a8416a36 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -3,7 +3,7 @@ Buchhalter Konto nicht (mehr) vorhanden - DAVx⁵-Adressbuch + Buchhalter-Adressbuch Adressbücher Feld wird benötigt Hilfe @@ -305,8 +305,8 @@ Keine Standard-Erinnerungen Wenn Standard-Erinnerungen für Termine ohne Erinnerung erzeugt werden sollen: gewünschte Anzahl der Minuten vor dem Ereignis. Leer lassen, um Standard-Erinnerungen zu deaktivieren. Kalenderfarben verwalten - Kalenderfarben werden bei jeder Synchronisierung neu gesetzt - Kalenderfarben können von anderen Apps festgesetzt werden + Kalenderfarben werden von Buchhalter verwaltet + Buchhalter setzt keine Kalenderfarben Unterstützung für Terminfarben Terminfarben werden synchronisiert Terminfarben werden nicht synchronisiert @@ -399,7 +399,7 @@ WebDAV-Upload WebDAV-Zugang - DAVx⁵-Berechtigungen + Buchhalter-Berechtigungen Zusätzliche Berechtigungen benötigt %s zu alt Benötigte Mindestversion: %1$s @@ -414,6 +414,6 @@ Ungültige Aufgabe vom Server erhalten Eine/mehrere ungültige Ressourcen ignoriert - DAVx⁵: Verbindungssicherheit - DAVx⁵ ist auf ein unbekanntes Zertifikat gestoßen. Ist dieses vertrauenswürdig? + Buchhalter: Verbindungssicherheit + Buchhalter ist auf ein unbekanntes Zertifikat gestoßen. Ist dieses vertrauenswürdig? diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index c3082eca4..51e5feb1c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -123,7 +123,7 @@ Reglamento de privacidad No hay conexión a Internet. Android no se sincronizará. Espacio de almacenamiento bajo. Android no ejecutará la sincronización. - Bienvenido a DAVx⁵!\n\nAhora puedes añadir una cuenta CalDAV/CardDAV. + Bienvenido a Gerente de cuentas!\n\nAhora puedes añadir una cuenta CalDAV/CardDAV. Sincronización automática del sistema completo está deshabilitada Activar Sincronizar todas las cuentas @@ -307,8 +307,8 @@ No se han creado recordatorios por defecto Si se crearán recordatorios por defecto para los eventos que no los tengan: el número de minutos antes del evento. Déjelo en blanco para deshabilitar los recordatorios por defecto. Colores de calendario - Los colores del calendario se restablecen en cada sincronización - Los colores del calendario pueden ser establecidos por otras aplicaciones + Los colores de los calendarios son administrados por Gerente de cuentas + Los colores de los calendarios no son establecidos por Gerente de cuentas Soporte de colores en eventos Los colores de los eventos están sincronizados Los colores de los eventos no están sincronizados @@ -401,7 +401,7 @@ Subiendo fichero WebDAV Montaje WebDAV - Permisos de DAVx⁵ + Permisos de Gerente de cuentas Permisos adicionales requeridos %s muy antiguo Mínima versión requerida: %1$s @@ -416,6 +416,6 @@ Tarea inválida recibidas del servidor Ignorando uno o más recursos inválidos - DAVx⁵: Seguridad de conexión - DAVx⁵ ha encontrado un certificado desconocido. ¿Quieres que sea válido? + Gerente de cuentas: Seguridad de conexión + Gerente de cuentas ha encontrado un certificado desconocido. ¿Quieres que sea válido? diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 46f1ad789..9257ca17b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -118,7 +118,7 @@ Politique de confidentialité Pas de connectivité Internet. Android ne pourra pas exécuter la synchronisation. L\'espace de stockage est presque plein. Android ne lancera pas la synchronisation. - Bienvenue sur DAVx⁵!\n\nVous pouvez maintenant ajouter un compte CalDAV ou CardDAV. + Bienvenue sur Gestionnaire de compte!\n\nVous pouvez maintenant ajouter un compte CalDAV ou CardDAV. La synchronisation automatique globale est désactivée Activer Synchroniser tous les comptes @@ -294,8 +294,8 @@ Aucun rappel par défaut Si des rappels par défaut doivent être créé pour des événements sans rappel: le nombre de minutes avant l\'événement. Laisser blanc pour désactiver les rappels par défaut. Choisir couleur du calendrier - Les couleurs du calendrier sont réinitialisées à chaque synchronisation - Les couleurs du calendrier peuvent être définies par d\'autres applications + Les couleurs de calendrier sont gérées par Gestionnaire de compte + Les couleurs de calendrier ne sont pas gérées par Gestionnaire de compte Couleur associée aux événements Les couleurs des événements sont synchronisées Les couleurs des événements sont pas synchronisées @@ -382,7 +382,7 @@ Téléchargement du fichier WebDAV Téléversement du fichier WebDAV - Autorisations DAVx⁵ + Autorisations Gestionnaire de compte Autorisations supplémentaires demandées %s trop vieux Version minimale requise : %1$s @@ -397,6 +397,6 @@ Reçu une tâche invalide du serveur Ignorer une ou plusieurs ressources non valides - DAVx⁵ : Sécurité de la connexion - DAVx⁵ a rencontré un certificat inconnu. Voulez-vous lui faire confiance? + Gestionnaire de compte : Sécurité de la connexion + Gestionnaire de compte a rencontré un certificat inconnu. Voulez-vous lui faire confiance? diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index ed61fb71c..7838c35ed 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -120,10 +120,9 @@ GYIK Közösség Támogatás - Adatvédelmi politika Nincs Internet-elérés. Az Android rendszer nem fogja elvégezni a szinkronizálst Kevés a rendelkezésre álló tárhely. A rendszer nem fog szinkronizálást végezni. - Üdvözöljük a DAVx⁵ felhasználók között!\n\nMost már felvehet CalDAV/CardDav fiókokat. + Üdvözöljük a Fiókkezelő felhasználók között!\n\nMost már felvehet CalDAV/CardDav fiókokat. A rendszerszintű automatikus szinkronizálás ki van kapcsolva Bekapcsolás Az összes fiók szinkronizálása @@ -305,8 +304,8 @@ Nem lesznek alapértelmezett emlékeztetők beállítva Ha szeretné, hogy az emlékeztető nélküli eseményekhez egy alapértelmezett emlékeztető legyen beállítva, akkor adja meg, hogy az hány perccel az esemény előtt legyen. Ha nem akar ilyet, hagyja üresen. Naptárszínek kezelése - Naptárszínek visszaállnak az alapértelmezettre minden szinkronizáláskor - Naptárszíneket más alkalmazásokban lehet beállítani + A naptárszíneket a Fiókkezelő kezeli + A naptárszíneket nem a Fiókkezelő kezeli Eseményszínek támogatása Eseményszínek szinkronizálva Eseményszínek nincsenek szinronizálva @@ -399,7 +398,7 @@ WebDAV fájl feltöltése WebDAV kötetek - DAVx⁵ engedélyek + Fiókkezelő engedélyek További engedélyek szükségesek %s túl régi Legalacsonyabb szükséges verzió: %1$s @@ -414,6 +413,6 @@ A szerver érvénytelen feladatot küldött Egy vagy több érvénytelen erőforrás kihagyva - DAVx⁵: kapcsolatbiztonság + Fiókkezelő: kapcsolatbiztonság Egy eddig ismeretlen tanúsítvány érkezett. Megbízhatónak kívánja elfogadni? diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index bd7dc4b96..6dc0151b9 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -114,7 +114,7 @@ Donazione Politica sulla riservatezza Nessuna connessione Internet. Android non eseguirà la sincronizzazione. - Benvenuto a DAVx⁵!\n\nÈ ora possibile aggiungere account CalDAV/CardDAV. + Benvenuto a Account Manager!\n\nÈ ora possibile aggiungere account CalDAV/CardDAV. La sincronizzazione automatica dell\'intero sistema è disabilitata Attiva Sincronizzazione di tutti gli account @@ -284,8 +284,8 @@ Indicare il numero di minuti che si desidera per il promemoria predefinito. Lasciare vuoto per non creare un promemoria predefinito. Cambia il colore del calendario - I colori del calendario sono resettati ad ogni sincronizzazione - I colori del calendario possono essere scelti da altre applicazioni + I colori dei calendari sono gestiti da Account Manager + I colori dei calendari non sono gestiti da Account Manager Supporto colore dell\'evento I colori degli eventi sono sincronizzati I colori degli eventi non sono sicnronizzati @@ -376,7 +376,7 @@ Lasciare vuoto per non creare un promemoria predefinito. Caricare file WebDAV Installazione WebDAV - Autorizzazioni DAVx⁵ + Autorizzazioni Account Manager Autorizzazioni addizionali richieste %s troppo vecchio Versione minima richiesta %1$s @@ -391,6 +391,6 @@ Lasciare vuoto per non creare un promemoria predefinito. Attività non valida ricevuta dal server Una o più risorse non valide ignorate - DAVx⁵: sicurezza della connessione - DAVx⁵ ha trovato un certificato sconosciuto. Ritenerlo affidabile? + Account Manager: sicurezza della connessione + Account Manager ha trovato un certificato sconosciuto. Ritenerlo affidabile? diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index dcc4274c6..31873cb04 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -46,7 +46,7 @@ FAQ 寄付 インターネット接続がありません。 Android は同期を実行しません。 - DAVx⁵ にようこそ!\n\nCalDAV/CardDAV アカウントを追加できるようになりました。 + アカウントマネージャー にようこそ!\n\nCalDAV/CardDAV アカウントを追加できるようになりました。 システム全体の自動同期が無効です 有効 @@ -170,6 +170,8 @@ この日数より過去のイベントは無視されます (0 も可)。すべてのイベントを同期させるには、空白のままにしてください。 カレンダーの色を管理 + カレンダーの色は アカウントマネージャー が管理します + カレンダーの色を アカウントマネージャー が設定しません イベントカラーサポート CardDAV 連絡先グループ方法 @@ -214,7 +216,7 @@ ユーザー名 パスワード - DAVx⁵ アクセス許可 + アカウントマネージャー アクセス許可 追加のアクセス許可が必要です 認証に失敗しました (ログイン情報を確認してください) ネットワークまたは I/O エラー – %s @@ -227,6 +229,6 @@ サーバーから無効なタスクを受信しました 1 または複数の無効なリソースを無視します - DAVx⁵: 接続セキュリティ - DAVx⁵は、未知の証明書を検出しました。それを信頼しますか? + アカウントマネージャー: 接続セキュリティ + アカウントマネージャーは、未知の証明書を検出しました。それを信頼しますか? diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index f00f7a73d..21c1a0623 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -1,8 +1,8 @@ - Account Manager - Account Manager-adressebok + Kontoadministrator + Kontoadministrator-adressebok Adressebøker Hjelp Behandle kontoer @@ -38,7 +38,7 @@ O-S-S Hjelp / Forum Doner - Velkommen til DAVx⁵.\n\nDu kan legge til en CalDAV/CardDAV-konto nå. + Velkommen til Kontoadministrator.\n\nDu kan legge til en CalDAV/CardDAV-konto nå. Systemomspennende automatisk synkronisering avskrudd Skru på @@ -163,6 +163,8 @@ Hendelser som er mer enn dette antallet dager i fortid vil bli ignorert (kan være 0). La stå tomt for å synkronisere alle hendelser. Velg kalenderfarger + Kalenderfarger behandles av Kontoadministrator + Kalenderfarger settes ikke av Kontoadministrator Støtte for fargelegging av hendelser CardDAV Kontaktgruppemetode @@ -201,7 +203,7 @@ En I/O-feil har inntruffet. Vis detaljer - DAVx⁵-tilganger + Kontoadministrator-tilganger Ytterligere tilganger kreves Nettverk- og I/O-feil - %s HTTP-tjenerfeil - %s @@ -209,6 +211,6 @@ Forsøk igjen Fikk ugyldig kontakt fra server - DAVx⁵: Tilkoblingssikkerhet - DAVx⁵ har støtt på et ukjent sertifikat. Har du tiltro til det? + Kontoadministrator: Tilkoblingssikkerhet + Kontoadministrator har støtt på et ukjent sertifikat. Har du tiltro til det? diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 4b89e90e5..60e79f31b 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -123,7 +123,7 @@ Privacybeleid Geen internetverbinding. Android synchroniseert niet. Er is te weinig opslagruimte. Android zal niet synchroniseren. - Welkom bij DAVx⁵!\n\nJe kunt nu een CalDAV/CardDAV account toevoegen. + Welkom bij Account Manager!\n\nJe kunt nu een CalDAV/CardDAv account toevoegen. Automatisch synchroniseren is voor alle accounts uitgeschakeld Inschakelen Alle accounts synchroniseren @@ -305,8 +305,8 @@ Wordt niet aangemaakt Vul het gewenste aantal minuten in. Leeg laten om herinneringen uit te schakelen. Kalender kleuren beheren - Worden bij elke sync teruggezet - Kunnen door andere apps worden ingesteld + Agenda kleuren worden door Account Manager beheerd. + Agenda kleuren worden niet door Account Manager ingesteld Gebeurtenis kleuren ondersteunen Worden gesynchroniseerd Worden niet gesynchroniseerd @@ -399,7 +399,7 @@ WebDAV-bestand uploaden WebDAV-koppeling - DAVx⁵ rechten + Account Manager rechten Aanvullende rechten vereist %ste oud Minimaal vereiste versie: %1$s @@ -414,6 +414,6 @@ Ongeldige taak ontvangen van server Een of meer ongeldige bronnen negeren - DAVx⁵: Beveiliging van de verbinding + Account Manager: Verbinding beveiliging DAVx⁵ is een onbekend certificaat tegengekomen. Moet het vertrouwd worden? diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 825fedafc..d71c2d0c2 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -123,7 +123,7 @@ Polityka prywatności Brak połączenia z Internetem. Android nie wykona synchronizacji. Za mało miejsca do przechowywania. Android nie uruchomi synchronizacji. - Witamy w DAVx⁵!\n\nMożesz teraz dodać konto CalDAV/CardDAV. + Witamy w Menadżer konta!\n\nMożesz teraz dodać konto CalDAV/CardDAV. Automatyczna synchronizacja dla całego systemu jest wyłączona Włącz Synchronizuj wszystkie konta @@ -309,8 +309,8 @@ Nie utworzono przypomnień domyślnych Jeżeli domyślne przypomnienia mają być utworzone dla zdarzeń bez przypomnienia: pożądana liczba minut przed zdarzeniem. Pozostaw puste aby wyłączyć domyślne przypomnienia. Zarządzaj kolorami kalendarza - Kolory kalendarza są resetowane przy każdej synchronizacji - Kolory kalendarza mogą być ustawiane przez inne aplikacje + Kolory kalendarza są zarządzane przez Menadżer konta + Kolory kalendarze nie są ustawiane przez Menadżer konta Obsługa kolorów wydarzeń Kolory wydarzeń są zsynchronizowane Kolory wydarzeń nie są zsynchronizowane @@ -403,7 +403,7 @@ Wgrywanie pliku WebDAV Punkt linkowania WebDAV - Uprawnienia DAVx⁵ + Uprawnienia Menadżer konta Wymagane dodatkowe uprawnienia %s zbyt stary/a Minimalna wymagana wersja: %1$s @@ -418,6 +418,6 @@ Otrzymano błędne zadanie z serwera Zignorowano jeden lub więcej nieważnych zasobów - DAVx⁵: Bezpieczeństwo połączenia - DAVx⁵ napotkał nieznany certyfikat. Czy chcesz go dodać? + Menadżer konta: Bezpieczeństwo połączenia + Menadżer konta napotkał nieznany certyfikat. Czy chcesz go dodać? diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5d2509bdc..e56225722 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -2,13 +2,13 @@ Менеджер по работе с клиентами - Адресная книга DAVx⁵ + Livro de endereços Gerente de contas Адресные книги Это поле является обязательным Помощь Управление аккаунтами Поделиться - База данных повреждена + База данных пaccount_title_address_bookовреждена Все учетные записи были удалены локально. Отладка Другие важные сообщения @@ -122,7 +122,7 @@ Политика конфиденциальности Нет подключения к интернету. Android не будет выполнять синхронизацию. Место для хранения мало. Android не будет выполнять синхронизацию. - Добро пожаловать в DAVx⁵!\n\nТеперь вы можете добавить аккаунт CalDAV/CardDAV. + Добро пожаловать в Менеджер по работе с клиентами\n\nТеперь вы можете добавить аккаунт CalDAV/CardDAV. Синхронизация отключена на уровне устройства Включить Синхронизировать все аккаунты @@ -308,8 +308,8 @@ Напоминания по умолчанию не создаются Для событий без напоминания введите желаемое количество минут для получения уведомления. Оставьте пустым, чтобы не использовать напоминания по умолчанию. Управление цветами календаря - Цвета календаря сбрасываются после каждой синхронизации - Цвета календаря могут быть установлены другими приложениями. + Цвета календаря управляются Менеджер по работе с клиентами + Цвета календаря не управляются Менеджер по работе с клиентами Поддержка цвета событий Цвета событий синхронизируются Цвета событий не синхронизируются @@ -402,7 +402,7 @@ Выгрузка файла WebDAV Точка монтирования WebDAV - Разрешения DAVx⁵ + Разрешения Менеджер по работе с клиентами Требуются дополнительные разрешения Приложение %s устарело Минимально необходимая версия: %1$s @@ -417,6 +417,6 @@ Получена недействительная задача от сервера Игнорирование одного или нескольких недействительных ресурсов - DAVx⁵: безопасность подключения - DAVx⁵ обнаружил неизвестный сертификат. Вы согласны ему доверять? + Менеджер по работе с клиентами: безопасность подключения + Менеджер по работе с клиентами обнаружил неизвестный сертификат. Вы хотите ему доверять? diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 4d5ca99e3..dde164550 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -28,7 +28,7 @@ Web sitesi SSS Bağış yap - DAVx⁵\'e hoşgeldin!\n\nŞimdi bir CalDAV/CardDAV hesabı ekleyebilirsin. + Muhasebe Müdürü\'e hoşgeldin!\n\nŞimdi bir CalDAV/CardDAV hesabı ekleyebilirsin. Servis keşfi başarısız Kolleksiyon listesi yenilenemedi @@ -107,6 +107,8 @@ Bu sayıdan daha eski olan olaylar yok sayılacaktır (0 olabilir). Tüm olayları senkronize etmek için boş bırak. Takvim renklerini yönet + Takvim renkleri Muhasebe Müdürü tarafından yönetilmekte + Takvim renkleri Muhasebe Müdürü tarafından ayarlanmadı Rehber yarat Benim Rehberim @@ -130,7 +132,7 @@ Kullanıcı adı Parola - DAVx⁵ izinleri + Muhasebe Müdürü izinleri Ek izinler zorunludur diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 24b28fdc0..d24ba638c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -77,7 +77,7 @@ Підтримка Політика конфіденційності Відсутнє підключення до інтернету. Android не може розпочати синхронізацію. - Вітаємо у DAVx⁵!\n\nТепер можете додавати облікові записи CalDAV/CardDAV. + Вітаємо у Менеджер рахунків!\n\nТепер можете додавати облікові записи CalDAV/CardDAV. Автоматичну синхронізацію вимкнено зі сторони системи Увімкнути Синхронізувати всі обліківки @@ -232,6 +232,8 @@ Нагадування за замовчуванням не створюються Якщо нагадування за замовчуванням створюються для подій без нагадування: бажана кількість хвилин до події. Залиште поле порожнім, щоб вимкнути нагадування за замовчуванням. Керування кольорами + Кольори календаря керуються Менеджер рахунків + Кольори календаря не керуються Менеджер рахунків Підтримка кольорів подій CardDAV Метод групування контактів @@ -286,7 +288,7 @@ Ім\'я користувача Пароль - Дозволи DAVx⁵ + Дозволи Менеджер рахунків Потребує додаткові дозволи Помилка аутентифікації (перевірте обліковий запис) Помилка мережі та вводу/виводу — %s @@ -299,6 +301,6 @@ Отримано помилкове завдання від сервера Ігнорування одного або більше хибних джерел - DAVx⁵: Безпека з\'єднання - DAVx⁵ зіткнувся з невідомим сертифікатом. Чи довіряти йому? + Менеджер рахунків: Безпека з\'єднання + Менеджер рахунків зіткнувся з невідомим сертифікатом. Чи довіряти йому? diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 007928f4a..d9b0277d0 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -55,7 +55,7 @@ 贊助我們 隱私權政策 網際網絡沒有連接。Android不會進行同步。 - 歡迎使用 DAVx⁵!\n\n您現在可以新增 CalDAV/CardDAV 帳號 + 歡迎使用 客户经理!\n\n您現在可以新增 CalDAV/CardDAV 帳號 操作系統的自動同步被關閉了 啟用 @@ -190,6 +190,8 @@ 如果默認的提醒方式在創建時沒有這一項:在事項前幾分鐘,就可以留空來關閉默認的提醒方式。 管理行事曆的顏色 設定事項的顔色 + 行事曆顏色由 客户经理 管理 + 行事曆顏色不由 客户经理 管理 CardDAV(聯絡人檔案) 聯絡人群組的儲存格式 @@ -232,7 +234,7 @@ 讀寫錯誤 顯示細節 - DAVx⁵ 權限 + 客户经理 權限 需要額外的權限 鑒權失敗(你需要檢查登錄憑證) 網際網絡或者輸入輸出錯誤——%s @@ -245,6 +247,6 @@ 收到了無效的任務 略過了一個或多個無效的資料 - DAVx⁵: 連線安全性 - DAVx⁵ 發現未知的憑證,您要信任它嗎? + 客户经理: 連線安全性 + 客户经理 發現未知的憑證,您要信任它嗎? diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 72da3cfa3..8adec81aa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -141,7 +141,7 @@ Privacy policy No Internet connectivity. Android will not run synchronization. Storage space low. Android will not run synchronization. - Welcome to DAVx⁵!\n\nYou can add a CalDAV/CardDAV account now. + Welcome to Account Manager!\n\nYou can add a CalDAV/CardDAV account now. System-wide automatic synchronization is disabled Enable Sync all accounts @@ -363,8 +363,8 @@ If default reminders shall be created for events without reminder: the desired number of minutes before the event. Leave blank to disable default reminders. manage_calendar_colors Manage calendar colors - Calendar colors are reset at each sync - Calendar colors can be set by other apps + Calendar colors are managed by Account Manager + Calendar colors are not set by Account Manager event_colors Event color support Event colors are synced @@ -472,7 +472,7 @@ WebDAV mount - DAVx⁵ permissions + Account Manager permissions Additional permissions required %s too old Minimum required version: %1$s @@ -488,7 +488,7 @@ Ignoring one or more invalid resources - DAVx⁵: Connection security - DAVx⁵ has encountered an unknown certificate. Do you want to trust it? + Account Manager: Connection security + Account Manager has encountered an unknown certificate. Do you want to trust it? -- GitLab From aeb4541eb928bcce9711b78d1af67a9544604d41 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 24 Aug 2022 16:55:02 +0200 Subject: [PATCH 028/285] Version bump to 4.2.3.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index dbe56e431..4df79136e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { defaultConfig { applicationId "at.bitfire.davdroid" - versionCode 402030100 - versionName '4.2.3.1-rc.1' + versionCode 402030101 + versionName '4.2.3.1' buildConfigField "long", "buildTime", System.currentTimeMillis() + "L" setProperty "archivesBaseName", "davx5-ose-" + getVersionName() -- GitLab From df417f73d15c8fd3c52bd6c38a6c99f68eb3c6cf Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Tue, 19 Jun 2018 10:11:03 +0530 Subject: [PATCH 029/285] Change default sync frequency of contacts and calendar. --- app/src/main/java/at/bitfire/davdroid/Constants.kt | 4 ++++ .../java/at/bitfire/davdroid/settings/AccountSettings.kt | 3 ++- .../main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt | 3 ++- .../at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt | 3 ++- app/src/main/res/values-da/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-hu/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-ja/strings.xml | 1 + app/src/main/res/values-nb-rNO/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + app/src/main/res/values/strings.xml | 6 ++++-- 15 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/Constants.kt b/app/src/main/java/at/bitfire/davdroid/Constants.kt index 8e6c59bc9..a53e0486b 100644 --- a/app/src/main/java/at/bitfire/davdroid/Constants.kt +++ b/app/src/main/java/at/bitfire/davdroid/Constants.kt @@ -10,6 +10,10 @@ object Constants { // gplay billing const val BILLINGCLIENT_CONNECTION_MAX_RETRIES = 4 + // NOTE: Android 7 and up don't allow 2 min sync frequencies unless system frameworks are modified + const val DEFAULT_CALENDAR_SYNC_INTERVAL = 2 * 60L // 2 minutes + const val DEFAULT_CONTACTS_SYNC_INTERVAL = 15 * 60L // 15 minutes + /** * Context label for [org.apache.commons.lang3.exception.ContextedException]. * Context value is the [at.bitfire.davdroid.resource.LocalResource] diff --git a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt index 3ab7e55d4..5a8324353 100644 --- a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt @@ -21,6 +21,7 @@ import android.util.Base64 import androidx.annotation.WorkerThread import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager +import at.bitfire.davdroid.Constants import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R import at.bitfire.davdroid.closeCompat @@ -755,7 +756,7 @@ class AccountSettings( // request sync of new address book account ContentResolver.setIsSyncable(account, context.getString(R.string.address_books_authority), 1) - setSyncInterval(context.getString(R.string.address_books_authority), 4*3600) + setSyncInterval(context.getString(R.string.address_books_authority), Constants.DEFAULT_CONTACTS_SYNC_INTERVAL) } /* Android 7.1.1 OpenTasks fix */ diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt index a808dd905..fafd7c0c4 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt @@ -17,6 +17,7 @@ import android.os.Build import androidx.annotation.WorkerThread import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import at.bitfire.davdroid.Constants import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.PermissionUtils import at.bitfire.davdroid.R @@ -133,7 +134,7 @@ object SyncUtils { ContentResolver.setIsSyncable(account, authority, 1) try { val settings = AccountSettings(context, account) - val interval = settings.getSavedTasksSyncInterval() ?: settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL) + val interval = settings.getSavedTasksSyncInterval() ?: Constants.DEFAULT_CALENDAR_SYNC_INTERVAL settings.setSyncInterval(authority, interval) } catch (e: InvalidAccountException) { // account has already been removed diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index f00e0e444..675232ea1 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -20,6 +20,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.* +import at.bitfire.davdroid.Constants import at.bitfire.davdroid.DavService import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R @@ -172,7 +173,7 @@ class AccountDetailsFragment : Fragment() { Logger.log.log(Level.INFO, "Writing account configuration to database", config) try { val accountSettings = AccountSettings(context, account) - val defaultSyncInterval = settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL) + val defaultSyncInterval = Constants.DEFAULT_CALENDAR_SYNC_INTERVAL val refreshIntent = Intent(context, DavService::class.java) refreshIntent.action = DavService.ACTION_REFRESH_COLLECTIONS diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 7432d0463..1a47b276d 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -263,6 +263,7 @@ Synkroniseringsinterval for opgaver Kun manuelt + Hvert andet minut Hvert 15. minut Hver halve time Hver time diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1a8416a36..fd1f090aa 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -263,6 +263,7 @@ Häufigkeit der Aufgaben-Synchronisierung Nur manuell + alle 2 Minuten Alle 15 Minuten Alle 30 Minuten Jede Stunde diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9257ca17b..253397d92 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -250,6 +250,7 @@ Intervalle de synchronisation des tâches Manuellement + Toutes les 2 minutes Tous les quarts d\'heure Toutes les demi-heures Toutes les heures diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 7838c35ed..ab327afc2 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -262,6 +262,7 @@ Feladatlisták szinkronizálásának sűrűsége Manális + 2 perc 15 percenként 30 percenként Óránként diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 6dc0151b9..1ef2d5667 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -239,6 +239,7 @@ Intervallo sincr. attività Solo manualmente + Ogni 2 minuti Ogni 15 minuti Ogni 30 minuti Ogni ora diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 31873cb04..03e564a4a 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -139,6 +139,7 @@ タスク同期間隔 手動のみ + 2 分ごと 15 分ごと 30 分ごと 1 時間ごと diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 21c1a0623..ad6fc8a44 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -132,6 +132,7 @@ Gjøremålssynkroniseringsintervall Bare manuelt + Hvert 2 minutter Hvert kvarter Hver halvtime Hver time diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d71c2d0c2..fac89b9ff 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -263,6 +263,7 @@ Częstotliwość synchronizacji list zadań Tylko ręcznie + Co 2 minuty Co 15 minut Co 30 minut Co godzinę diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e56225722..568b1a840 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -262,6 +262,7 @@ Интервал синхронизации задач Только вручную + Каждые 2 минуты Каждые 15 минут Каждые 30 минут Каждый час diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d24ba638c..2d5d1b4ce 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -187,6 +187,7 @@ Інтервал синхронізації завдань Вручну + Кожні 2 хвилини Кожні 15 хвилин Кожні 30 хвилин Щогодинно diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8adec81aa..ef0203a8d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -302,7 +302,8 @@ sync_interval_tasks Tasks sync. interval - -1 + -1 + 120 900 1800 3600 @@ -311,7 +312,8 @@ 86400 - Only manually + Only manually + Every 2 minutes Every 15 minutes Every 30 minutes Every hour -- GitLab From 5fc99687febd92209950e446ffeea9e1e7e2f34a Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Tue, 19 Jun 2018 04:56:14 +0000 Subject: [PATCH 030/285] Update .gitlab-ci.yml --- .gitlab-ci.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..0634e5747 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,22 @@ +image: "registry.gitlab.eelo.io:5000/eelo/docker-android-apps-cicd:latest" + +stages: + - build + +before_script: + - git submodule update --init --recursive + - export GRADLE_USER_HOME=$(pwd)/.gradle + - chmod +x ./gradlew + +cache: + key: ${CI_PROJECT_ID} + paths: + - .gradle/ + +build: + stage: build + script: + - ./gradlew assembleDebug + artifacts: + paths: + - app/build/outputs/apk/standard/debug/app-standard-debug.apk -- GitLab From 0d8eba09e0cd289e19f1f4f6ffae849c9ebf90f7 Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Tue, 19 Jun 2018 12:57:04 +0530 Subject: [PATCH 031/285] Handle exceptions during login. --- .../ui/setup/GoogleAuthenticatorFragment.kt | 102 ++++++++++++------ 1 file changed, 71 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt index 66eb63979..4ff62eb5b 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt @@ -20,6 +20,7 @@ import android.app.Activity import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.net.ConnectivityManager import android.os.AsyncTask import android.os.Bundle import android.os.Handler @@ -27,13 +28,13 @@ import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProviders import at.bitfire.davdroid.R import at.bitfire.davdroid.authorization.IdentityProvider import at.bitfire.davdroid.databinding.FragmentGoogleAuthenticatorBinding import at.bitfire.davdroid.db.Credentials -import kotlinx.android.synthetic.main.fragment_google_authenticator.* import net.openid.appauth.* import org.json.JSONException import org.json.JSONObject @@ -46,7 +47,6 @@ import java.net.MalformedURLException import java.net.URI import java.net.URL - class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenResponseCallback { private lateinit var model: GoogleAuthenticatorModel @@ -61,11 +61,22 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon private val bufferSize = 1024 private var userInfoJson: JSONObject? = null + private fun isNetworkAvailable(): Boolean { + val connectivityManager = activity!!.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetworkInfo = connectivityManager.activeNetworkInfo + return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - model = ViewModelProviders.of(this).get(GoogleAuthenticatorModel::class.java) + savedInstanceState: Bundle?): View { + model = ViewModelProviders.of(this)[GoogleAuthenticatorModel::class.java] loginModel = ViewModelProviders.of(requireActivity())[LoginModel::class.java] + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } + // Initialise the authorization service authorizationService = AuthorizationService(context!!) @@ -87,7 +98,8 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon makeAuthRequest(serviceConfiguration, idp) } else { - // TODO Handle error + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + activity!!.finish() } } @@ -99,15 +111,16 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon } else { if (authState == null) { - val response = AuthorizationResponse.fromIntent(activity!!.intent) + val response = AuthorizationResponse.fromIntent(activity!!.intent) val ex = AuthorizationException.fromIntent(activity!!.intent) authState = AuthState(response, ex) if (response != null) { - exchangeAuthorizationCode(response) + exchangeAuthorizationCode(response) } else { - // TODO Handle error + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + activity!!.finish() } } } @@ -120,6 +133,11 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon serviceConfig: AuthorizationServiceConfiguration, idp: IdentityProvider) { + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } + val authRequest = AuthorizationRequest.Builder( serviceConfig, idp.clientId, @@ -137,8 +155,8 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon idp.clientSecret), authorizationService?.createCustomTabsIntentBuilder()!! .build()) - - requireActivity().setResult(Activity.RESULT_OK) + + requireActivity().setResult(Activity.RESULT_OK) requireActivity().finish() } @@ -164,6 +182,11 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon } private fun exchangeAuthorizationCode(authorizationResponse: AuthorizationResponse) { + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } + val additionalParams = HashMap() if (getClientSecretFromIntent(activity!!.intent) != null) { additionalParams["client_secret"] = getClientSecretFromIntent(activity!!.intent) @@ -174,12 +197,16 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon private fun getClientSecretFromIntent(intent: Intent): String? { return if (!intent.hasExtra(extraClientSecret)) { null - } - else intent.getStringExtra(extraClientSecret) + } else intent.getStringExtra(extraClientSecret) } private fun performTokenRequest(request: TokenRequest) { + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } + authorizationService?.performTokenRequest( request, this) } @@ -190,17 +217,27 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon } private fun getAccountInfo() { + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } + val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent) if (!authState!!.isAuthorized || discoveryDoc == null || discoveryDoc.userinfoEndpoint == null) { - //TODO Error occurred + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + activity!!.finish() } else { object : AsyncTask() { override fun doInBackground(vararg params: Void): Void? { - fetchUserInfo() + if (fetchUserInfo()) { + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + activity!!.finish() + } + return null } }.execute() @@ -224,15 +261,16 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon } - private fun fetchUserInfo() { + private fun fetchUserInfo(): Boolean { + var error = false + if (authState!!.authorizationServiceConfiguration == null) { - // TODO Handle error due to unavailable service configuration - return + return true } authState!!.performActionWithFreshTokens(authorizationService!!, AuthState.AuthStateAction { accessToken, _, ex -> if (ex != null) { - // TODO An exception occurred, handle error + error = true return@AuthStateAction } @@ -244,7 +282,7 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon userInfoEndpoint = URL(discoveryDoc.userinfoEndpoint!!.toString()) } catch (urlEx: MalformedURLException) { - // TODO Handle error due to malformed URL + error = true return@AuthStateAction } @@ -258,10 +296,10 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon updateUserInfo(JSONObject(response)) } catch (ioEx: IOException) { - // TODO Handle network error + error = true } catch (jsonEx: JSONException) { - // TODO Handle JSON parse error + error = true } finally { if (userInfoResponse != null) { @@ -269,12 +307,14 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon userInfoResponse.close() } catch (ioEx: IOException) { - // TODO Handle network exception while closing response stream + error = true } } } }) + + return error } @Throws(IOException::class) @@ -298,6 +338,11 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon } private fun onAccountInfoGotten() { + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } + if (userInfoJson != null) { try { var emailAddress = "" @@ -311,18 +356,16 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon .addToBackStack(null) .commit() - /*account.setName(name) - account.setEmailAddress(emailAddress) - account.setAuthToken(authState!!.accessToken) - account.setRefreshToken(authState!!.refreshToken)*/ } catch (ex: JSONException) { - // TODO Handle JSON parse error + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + activity!!.finish() } } else { - //TODO Handle error + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + activity!!.finish() } } @@ -360,7 +403,4 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon super.onDestroy() authorizationService?.dispose() } - - } - -- GitLab From 4672654d4592ed49dd4ded5733240b7110f23f93 Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Thu, 21 Jun 2018 10:15:49 +0530 Subject: [PATCH 032/285] Implement OAuth authorisation and authentication for eelo accounts. --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 21 +- .../authorization/IdentityProvider.java | 14 +- .../ui/setup/EeloAuthenticatorFragment.kt | 447 ++++++++++++++++++ .../ui/setup/EeloAuthenticatorModel.kt | 63 +++ .../davdroid/ui/setup/LoginActivity.kt | 7 +- .../layout/fragment_eelo_authenticator.xml | 26 + ....xml => account_providers_auth_config.xml} | 11 + 8 files changed, 565 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/at/bitfire/davdroid/ui/setup/EeloAuthenticatorFragment.kt create mode 100644 app/src/main/java/at/bitfire/davdroid/ui/setup/EeloAuthenticatorModel.kt create mode 100644 app/src/main/res/layout/fragment_eelo_authenticator.xml rename app/src/main/res/values/{email_providers_auth_config.xml => account_providers_auth_config.xml} (59%) diff --git a/app/build.gradle b/app/build.gradle index bf45cbef8..7fe082fe3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,8 +96,8 @@ android { } defaultConfig { - manifestPlaceholders = [ - 'appAuthRedirectScheme': 'com.googleusercontent.apps.628867657910-7ade6gut5rhabdgjq6k4rln9i1u9ppca' + manifestPlaceholders = [ + 'appAuthRedirectScheme': 'net.openid.appauthdemo' ] } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 19c8529de..7ee75a39c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -302,29 +302,10 @@ - + - - - - - - - - - - PROVIDERS = Arrays.asList( - GOOGLE); + GOOGLE, + EELO); public static List getEnabledProviders(Context context) { ArrayList providers = new ArrayList<>(); diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/EeloAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/EeloAuthenticatorFragment.kt new file mode 100644 index 000000000..c2bff6509 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/EeloAuthenticatorFragment.kt @@ -0,0 +1,447 @@ +/* + * Copyright ECORP SAS 2022 + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 at.bitfire.davdroid.ui.setup + +import android.app.Activity +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.os.AsyncTask +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProviders +import at.bitfire.davdroid.R +import at.bitfire.davdroid.authorization.IdentityProvider +import at.bitfire.davdroid.databinding.FragmentEeloAuthenticatorBinding +import at.bitfire.davdroid.db.Credentials +import net.openid.appauth.* +import org.json.JSONException +import org.json.JSONObject +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.net.HttpURLConnection +import java.net.MalformedURLException +import java.net.URI +import java.net.URL + +class EeloAuthenticatorFragment : Fragment(), AuthorizationService.TokenResponseCallback { + + private lateinit var model: EeloAuthenticatorModel + private lateinit var loginModel: LoginModel + + private val extraAuthServiceDiscovery = "authServiceDiscovery" + private val extraClientSecret = "clientSecret" + + private var authState: AuthState? = null + private var authorizationService: AuthorizationService? = null + + private val bufferSize = 1024 + private var userInfoJson: JSONObject? = null + + private fun isNetworkAvailable(): Boolean { + val connectivityManager = + activity!!.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetworkInfo = connectivityManager.activeNetworkInfo + return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + + model = ViewModelProviders.of(this).get(EeloAuthenticatorModel::class.java) + loginModel = ViewModelProviders.of(requireActivity()).get(LoginModel::class.java) + + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG) + .show() + activity!!.finish() + } + + // Initialise the authorization service + authorizationService = AuthorizationService(context!!) + + val v = FragmentEeloAuthenticatorBinding.inflate(inflater, container, false) + v.lifecycleOwner = this + v.model = model + + activity?.intent?.let { + model.initialize(it) + + if (!with(it) { + getBooleanExtra( + LoginActivity.ACCOUNT_PROVIDER_EELO_AUTH_COMPLETE, + false + ) + }) { + // Get all the account providers + val providers = IdentityProvider.getEnabledProviders(context) + + // Iterate over the account providers + for (idp in providers) { + val retrieveCallback = + AuthorizationServiceConfiguration.RetrieveConfigurationCallback { serviceConfiguration, ex -> + if (ex == null && serviceConfiguration != null) { + makeAuthRequest(serviceConfiguration, idp) + } else { + Toast.makeText( + context, + "Login failed, please try again later", + Toast.LENGTH_LONG + ).show() + activity!!.finish() + } + } + + if (idp.name == getString(R.string.google_name)) { + // Get configurations for the Google account provider + idp.retrieveConfig(context, retrieveCallback) + } + } + } else { + if (authState == null) { + val response = AuthorizationResponse.fromIntent(activity!!.intent) + val ex = AuthorizationException.fromIntent(activity!!.intent) + authState = AuthState(response, ex) + + if (response != null) { + exchangeAuthorizationCode(response) + } else { + Toast.makeText( + context, + "Login failed, please try again later", + Toast.LENGTH_LONG + ).show() + activity!!.finish() + } + } + } + } + + return v.root + } + + private fun makeAuthRequest(serviceConfig: AuthorizationServiceConfiguration, idp: IdentityProvider) { + + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG) + .show() + activity!!.finish() + } + + val authRequest = AuthorizationRequest.Builder( + serviceConfig, + idp.clientId, + ResponseTypeValues.CODE, + idp.redirectUri + ) + .setScope(idp.scope) + .build() + + authorizationService?.performAuthorizationRequest( + authRequest, + createPostAuthorizationIntent( + context!!, + authRequest, + serviceConfig.discoveryDoc, + idp.clientSecret + ), + authorizationService?.createCustomTabsIntentBuilder()!! + .build() + ) + + requireActivity().setResult(Activity.RESULT_OK) + requireActivity().finish() + } + + private fun createPostAuthorizationIntent( + context: Context, + request: AuthorizationRequest, + discoveryDoc: AuthorizationServiceDiscovery?, + clientSecret: String? + ): PendingIntent { + val intent = Intent(context, LoginActivity::class.java) + + if (discoveryDoc != null) { + intent.putExtra(extraAuthServiceDiscovery, discoveryDoc.docJson.toString()) + } + + if (clientSecret != null) { + intent.putExtra(extraClientSecret, clientSecret) + } + + intent.putExtra( + LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, + LoginActivity.ACCOUNT_PROVIDER_EELO + ) + intent.putExtra(LoginActivity.ACCOUNT_PROVIDER_EELO_AUTH_COMPLETE, true) + + return PendingIntent.getActivity(context, request.hashCode(), intent, 0) + } + + private fun exchangeAuthorizationCode(authorizationResponse: AuthorizationResponse) { + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG) + .show() + activity!!.finish() + } + + val additionalParams = HashMap() + + if (getClientSecretFromIntent(activity!!.intent) != null) { + additionalParams["client_secret"] = getClientSecretFromIntent(activity!!.intent) + } + + performTokenRequest(authorizationResponse.createTokenExchangeRequest(additionalParams)) + } + + private fun getClientSecretFromIntent(intent: Intent): String? { + return if (!intent.hasExtra(extraClientSecret)) { + null + } else intent.getStringExtra(extraClientSecret) + } + + + private fun performTokenRequest(request: TokenRequest) { + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG) + .show() + activity!!.finish() + } + + authorizationService?.performTokenRequest(request, this) + } + + override fun onTokenRequestCompleted(response: TokenResponse?, ex: AuthorizationException?) { + authState?.update(response, ex) + getAccountInfo() + } + + private fun getAccountInfo() { + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG) + .show() + activity!!.finish() + } + + val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent) + + if (!authState!!.isAuthorized || discoveryDoc == null || discoveryDoc.userinfoEndpoint == null) { + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG) + .show() + activity!!.finish() + } else { + object : AsyncTask() { + override fun doInBackground(vararg params: Void): Void? { + + if (fetchUserInfo()) { + Toast.makeText( + context, + "Login failed, please try again later", + Toast.LENGTH_LONG + ).show() + activity!!.finish() + } + + return null + } + }.execute() + } + } + + private fun getDiscoveryDocFromIntent(intent: Intent): AuthorizationServiceDiscovery? { + if (!intent.hasExtra(extraAuthServiceDiscovery)) { + return null + } + + val discoveryJson = intent.getStringExtra(extraAuthServiceDiscovery) + + try { + return AuthorizationServiceDiscovery(JSONObject(discoveryJson)) + } catch (ex: JSONException) { + throw IllegalStateException("Malformed JSON in discovery doc") + } catch (ex: AuthorizationServiceDiscovery.MissingArgumentException) { + throw IllegalStateException("Malformed JSON in discovery doc") + } + } + + private fun fetchUserInfo(): Boolean { + var error = false + + if (authState!!.authorizationServiceConfiguration == null) { + return true + } + + authState!!.performActionWithFreshTokens( + authorizationService!!, + AuthState.AuthStateAction { accessToken, _, ex -> + + if (ex != null) { + error = true + return@AuthStateAction + } + + val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent) + ?: throw IllegalStateException("no available discovery doc") + val userInfoEndpoint: URL + + try { + userInfoEndpoint = URL(discoveryDoc.userinfoEndpoint!!.toString()) + } catch (urlEx: MalformedURLException) { + error = true + return@AuthStateAction + } + + var userInfoResponse: InputStream? = null + + try { + val conn = userInfoEndpoint.openConnection() as HttpURLConnection + conn.setRequestProperty("Authorization", "Bearer " + accessToken!!) + conn.instanceFollowRedirects = false + userInfoResponse = conn.inputStream + val response = readStream(userInfoResponse) + updateUserInfo(JSONObject(response)) + } catch (ioEx: IOException) { + error = true + } catch (jsonEx: JSONException) { + error = true + } finally { + + if (userInfoResponse != null) { + try { + userInfoResponse.close() + } catch (ioEx: IOException) { + error = true + } + } + + } + }) + + return error + } + + @Throws(IOException::class) + private fun readStream(stream: InputStream?): String { + val br = BufferedReader(InputStreamReader(stream!!)) + val buffer = CharArray(bufferSize) + val sb = StringBuilder() + var readCount = br.read(buffer) + + while (readCount != -1) { + sb.append(buffer, 0, readCount) + readCount = br.read(buffer) + } + + return sb.toString() + } + + private fun updateUserInfo(jsonObject: JSONObject) { + Handler(Looper.getMainLooper()).post { + userInfoJson = jsonObject + onAccountInfoGotten() + } + } + + private fun onAccountInfoGotten() { + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG) + .show() + activity!!.finish() + } + + if (userInfoJson != null) { + try { + var emailAddress = "" + + if (userInfoJson!!.has("email")) { + emailAddress = userInfoJson!!.getString("email") + } + + if (validate(emailAddress, authState!!)) { + requireFragmentManager().beginTransaction() + .replace(android.R.id.content, DetectConfigurationFragment(), null) + .addToBackStack(null) + .commit() + } + + } catch (ex: JSONException) { + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG) + .show() + activity!!.finish() + } + + } else { + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG) + .show() + activity!!.finish() + } + + } + + private fun validate(emailAddress: String, authState: AuthState): Boolean { + var valid = false + + fun validateUrl() { + model.baseUrlError.value = null + try { + val uri = URI("https://apidata.googleusercontent.com/caldav/v2/$emailAddress/events") + + if (uri.scheme.equals("http", true) || uri.scheme.equals("https", true)) { + valid = true + loginModel.baseURI = uri + } else { + model.baseUrlError.value = getString(R.string.login_url_must_be_http_or_https) + } + + } catch (e: Exception) { + model.baseUrlError.value = e.localizedMessage + } + } + + when { + + model.loginWithUrlAndTokens.value == true -> { + validateUrl() + model.usernameError.value = null + + if (loginModel.baseURI != null) { + valid = true + loginModel.credentials = Credentials(emailAddress, null, authState, null) + } + } + + } + + return valid + } + + override fun onDestroy() { + super.onDestroy() + authorizationService?.dispose() + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/EeloAuthenticatorModel.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/EeloAuthenticatorModel.kt new file mode 100644 index 000000000..77fa1352d --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/EeloAuthenticatorModel.kt @@ -0,0 +1,63 @@ +/* + * Copyright ECORP SAS 2022 + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 at.bitfire.davdroid.ui.setup + +import android.content.Intent +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class EeloAuthenticatorModel : ViewModel() { + + private var initialized = false + + val loginWithUrlAndTokens = MutableLiveData() + + val baseUrl = MutableLiveData() + val baseUrlError = MutableLiveData() + + val emailAddress = MutableLiveData() + val emailAddressError = MutableLiveData() + + val username = MutableLiveData() + val usernameError = MutableLiveData() + + val password = MutableLiveData() + val passwordError = MutableLiveData() + + val certificateAlias = MutableLiveData() + val certificateAliasError = MutableLiveData() + + init { + loginWithUrlAndTokens.value = true + } + + fun initialize(intent: Intent) { + if (initialized) + return + + // we've got initial login data + val givenUrl = intent.getStringExtra(LoginActivity.EXTRA_URL) + val givenUsername = intent.getStringExtra(LoginActivity.EXTRA_USERNAME) + val givenPassword = intent.getStringExtra(LoginActivity.EXTRA_PASSWORD) + + baseUrl.value = givenUrl + + password.value = givenPassword + + initialized = true + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt index f963eb96a..47f689139 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt @@ -46,6 +46,7 @@ class LoginActivity: AppCompatActivity() { const val ACCOUNT_PROVIDER_EELO = "eelo" const val ACCOUNT_PROVIDER_GOOGLE = "google" const val ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE = "google_auth_complete" + const val ACCOUNT_PROVIDER_EELO_AUTH_COMPLETE = "eelo_auth_complete" } @Inject @@ -69,11 +70,8 @@ class LoginActivity: AppCompatActivity() { if (fragment != null) { when (intent.getStringExtra(SETUP_ACCOUNT_PROVIDER_TYPE)) { ACCOUNT_PROVIDER_EELO -> { - // Set the eelo Contacts and Calendar service URL - intent.putExtra(EXTRA_URL, "https://drive.eelo.io") - // first call, add first login fragment supportFragmentManager.beginTransaction() - .replace(android.R.id.content, fragment) + .replace(android.R.id.content, EeloAuthenticatorFragment()) .commit() } ACCOUNT_PROVIDER_GOOGLE -> { @@ -101,5 +99,4 @@ class LoginActivity: AppCompatActivity() { UiUtils.launchUri(this, App.homepageUrl(this).buildUpon().appendPath("tested-with").build()) } - } diff --git a/app/src/main/res/layout/fragment_eelo_authenticator.xml b/app/src/main/res/layout/fragment_eelo_authenticator.xml new file mode 100644 index 000000000..a7752a722 --- /dev/null +++ b/app/src/main/res/layout/fragment_eelo_authenticator.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/email_providers_auth_config.xml b/app/src/main/res/values/account_providers_auth_config.xml similarity index 59% rename from app/src/main/res/values/email_providers_auth_config.xml rename to app/src/main/res/values/account_providers_auth_config.xml index 22a32a324..51e2360ff 100644 --- a/app/src/main/res/values/email_providers_auth_config.xml +++ b/app/src/main/res/values/account_providers_auth_config.xml @@ -14,5 +14,16 @@ com.googleusercontent.apps.100496780587-pbiu5eudcjm6cge2phduc6mt8mgbsmsr:/oauth2redirect + + eelo + eelo + + https://identity.test.eelo.io/auth/realms/eelo/.well-known/openid-configuration + + openid email profile + + net.openid.appauthdemo://oauth2redirect + + -- GitLab From e85925e0848aad13d8c399475e71d70db7ec2756 Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Fri, 22 Jun 2018 10:23:04 +0530 Subject: [PATCH 033/285] Show a message upon successfully retrieving an OAuth access token for eelo accounts. --- .../ui/setup/GoogleAuthenticatorFragment.kt | 5 ++++- .../main/res/layout/fragment_eelo_authenticator.xml | 13 ++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt index 4ff62eb5b..f1d937f60 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt @@ -35,6 +35,7 @@ import at.bitfire.davdroid.R import at.bitfire.davdroid.authorization.IdentityProvider import at.bitfire.davdroid.databinding.FragmentGoogleAuthenticatorBinding import at.bitfire.davdroid.db.Credentials +import kotlinx.android.synthetic.main.fragment_eelo_authenticator.* import net.openid.appauth.* import org.json.JSONException import org.json.JSONObject @@ -213,7 +214,9 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon override fun onTokenRequestCompleted(response: TokenResponse?, ex: AuthorizationException?) { authState?.update(response, ex) - getAccountInfo() + + progress_bar.visibility = View.GONE + successful_oauth_text_view.visibility = View.VISIBLE } private fun getAccountInfo() { diff --git a/app/src/main/res/layout/fragment_eelo_authenticator.xml b/app/src/main/res/layout/fragment_eelo_authenticator.xml index a7752a722..462c23a5b 100644 --- a/app/src/main/res/layout/fragment_eelo_authenticator.xml +++ b/app/src/main/res/layout/fragment_eelo_authenticator.xml @@ -19,7 +19,18 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" /> - + + + -- GitLab From 1d1636eebaa11759efe813c597dda78412178c6a Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Wed, 11 Jul 2018 15:51:22 +0530 Subject: [PATCH 034/285] Undo using custom account types for Google and eelo accounts and redo everything else --- app/src/main/AndroidManifest.xml | 2 +- app/src/main/res/values-de/strings.xml | 14 +++++++------- .../res/values/account_providers_auth_config.xml | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7ee75a39c..480ac9c1c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -61,7 +61,7 @@ android:exported="true"> - + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index fd1f090aa..8d0b8b20c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,9 +1,9 @@ - Buchhalter + Konten Manager Konto nicht (mehr) vorhanden - Buchhalter-Adressbuch + Konten Manager-Adressbuch Adressbücher Feld wird benötigt Hilfe @@ -306,8 +306,8 @@ Keine Standard-Erinnerungen Wenn Standard-Erinnerungen für Termine ohne Erinnerung erzeugt werden sollen: gewünschte Anzahl der Minuten vor dem Ereignis. Leer lassen, um Standard-Erinnerungen zu deaktivieren. Kalenderfarben verwalten - Kalenderfarben werden von Buchhalter verwaltet - Buchhalter setzt keine Kalenderfarben + Kalenderfarben werden von Konten Manager verwaltet + Konten Manager setzt keine Kalenderfarben Unterstützung für Terminfarben Terminfarben werden synchronisiert Terminfarben werden nicht synchronisiert @@ -400,7 +400,7 @@ WebDAV-Upload WebDAV-Zugang - Buchhalter-Berechtigungen + Konten Manager-Berechtigungen Zusätzliche Berechtigungen benötigt %s zu alt Benötigte Mindestversion: %1$s @@ -415,6 +415,6 @@ Ungültige Aufgabe vom Server erhalten Eine/mehrere ungültige Ressourcen ignoriert - Buchhalter: Verbindungssicherheit - Buchhalter ist auf ein unbekanntes Zertifikat gestoßen. Ist dieses vertrauenswürdig? + Konten Manager: Verbindungssicherheit + Konten Manager ist auf ein unbekanntes Zertifikat gestoßen. Ist dieses vertrauenswürdig? diff --git a/app/src/main/res/values/account_providers_auth_config.xml b/app/src/main/res/values/account_providers_auth_config.xml index 51e2360ff..59c232ef5 100644 --- a/app/src/main/res/values/account_providers_auth_config.xml +++ b/app/src/main/res/values/account_providers_auth_config.xml @@ -18,7 +18,7 @@ eelo eelo - https://identity.test.eelo.io/auth/realms/eelo/.well-known/openid-configuration + https://identity.test.eelo.io/auth/realms/My-realm/.well-known/openid-configuration openid email profile -- GitLab From 5d8cc40c8539745947ecbdad8460606a0546ba2e Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Wed, 11 Jul 2018 22:36:51 +0530 Subject: [PATCH 035/285] Show eelo and Google calendar accounts in their own custom account types --- .../java/at/bitfire/davdroid/DavService.kt | 2 +- .../at/bitfire/davdroid/db/AppDatabase.kt | 5 +- .../java/at/bitfire/davdroid/db/Service.kt | 1 + .../syncadapter/AccountsUpdatedListener.kt | 44 ++-- .../EeloAccountAuthenticatorService.kt | 14 +- .../EeloCalendarsSyncAdapterService.kt | 192 +++++++++++++++++ .../EeloTasksSyncAdapterService.kt | 203 ++++++++++++++++++ .../GoogleAccountAuthenticatorService.kt | 13 +- .../GoogleCalendarsSyncAdapterService.kt | 190 ++++++++++++++++ .../GoogleTasksSyncAdapterService.kt | 201 +++++++++++++++++ .../bitfire/davdroid/syncadapter/SyncUtils.kt | 2 +- .../davdroid/syncadapter/TasksSyncManager.kt | 2 +- .../davdroid/ui/AccountListFragment.kt | 11 +- .../bitfire/davdroid/ui/DebugInfoActivity.kt | 6 +- .../ui/setup/AccountDetailsFragment.kt | 41 +++- app/src/main/res/xml/eelo_sync_calendars.xml | 16 ++ app/src/main/res/xml/eelo_sync_tasks.xml | 15 ++ .../main/res/xml/google_sync_calendars.xml | 17 ++ app/src/main/res/xml/google_sync_tasks.xml | 15 ++ 19 files changed, 949 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/EeloCalendarsSyncAdapterService.kt create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/EeloTasksSyncAdapterService.kt create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleCalendarsSyncAdapterService.kt create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleTasksSyncAdapterService.kt create mode 100644 app/src/main/res/xml/eelo_sync_calendars.xml create mode 100644 app/src/main/res/xml/eelo_sync_tasks.xml create mode 100644 app/src/main/res/xml/google_sync_calendars.xml create mode 100644 app/src/main/res/xml/google_sync_tasks.xml diff --git a/app/src/main/java/at/bitfire/davdroid/DavService.kt b/app/src/main/java/at/bitfire/davdroid/DavService.kt index 85a84244d..4935c976d 100644 --- a/app/src/main/java/at/bitfire/davdroid/DavService.kt +++ b/app/src/main/java/at/bitfire/davdroid/DavService.kt @@ -172,7 +172,7 @@ class DavService: IntentService("DavService") { val collectionDao = db.collectionDao() val service = db.serviceDao().get(serviceId) ?: throw IllegalArgumentException("Service not found") - val account = Account(service.accountName, getString(R.string.account_type)) + val account = Account(service.accountName, service.accountType) val homeSets = homeSetDao.getByService(serviceId).associateBy { it.url }.toMutableMap() val collections = collectionDao.getByService(serviceId).associateBy { it.url }.toMutableMap() diff --git a/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt b/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt index 6cc264093..31475b71a 100644 --- a/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt +++ b/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt @@ -116,12 +116,13 @@ abstract class AppDatabase: RoomDatabase() { "CREATE TABLE service(" + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," + "accountName TEXT NOT NULL," + - "authState TEXT ," + + "authState TEXT," + + "accountType TEXT," + "type TEXT NOT NULL," + "principal TEXT DEFAULT NULL" + ")", "CREATE UNIQUE INDEX index_service_accountName_type ON service(accountName, type)", - "INSERT INTO service(id, accountName, authState, type, principal) SELECT _id, accountName, authState, service, principal FROM services", + "INSERT INTO service(id, accountName, authState, accountType, type, principal) SELECT _id, accountName, authState, accountType, service, principal FROM services", "DROP TABLE services", // migrate "homesets" to "homeset": rename columns, make id NOT NULL diff --git a/app/src/main/java/at/bitfire/davdroid/db/Service.kt b/app/src/main/java/at/bitfire/davdroid/db/Service.kt index 066bb234b..ca024482b 100644 --- a/app/src/main/java/at/bitfire/davdroid/db/Service.kt +++ b/app/src/main/java/at/bitfire/davdroid/db/Service.kt @@ -21,6 +21,7 @@ data class Service( var accountName: String, var authState: String?, + var accountType: String, var type: String, diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountsUpdatedListener.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountsUpdatedListener.kt index 68804cb70..c2e85d2c7 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountsUpdatedListener.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountsUpdatedListener.kt @@ -68,34 +68,38 @@ class AccountsUpdatedListener private constructor( @Synchronized private fun cleanupAccounts(context: Context, accounts: Array) { - Logger.log.log(Level.INFO, "Cleaning up accounts. Current accounts:", accounts) + Logger.log.log(Level.INFO, "Cleaning up accounts. Current accounts") - val mainAccountType = context.getString(R.string.account_type) - val mainAccountNames = accounts - .filter { account -> account.type == mainAccountType } - .map { it.name } + val accountManager = AccountManager.get(context) + val accountNames = HashSet() + val accountFromManager = ArrayList() - val addressBookAccountType = context.getString(R.string.account_type_address_book) - val addressBooks = accounts - .filter { account -> account.type == addressBookAccountType } - .map { addressBookAccount -> LocalAddressBook(context, addressBookAccount, null) } - for (addressBook in addressBooks) { - try { - if (!mainAccountNames.contains(addressBook.mainAccount.name)) - // the main account for this address book doesn't exist anymore - addressBook.delete() - } catch(e: Exception) { - Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e) - } + accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)).forEach { accountFromManager.add(it) } + accountManager.getAccountsByType(context.getString(R.string.google_account_type)).forEach { accountFromManager.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type)).forEach { accountFromManager.add(it) } + + for (account in accountFromManager.toTypedArray()) { + accountNames += account.name } + // delete orphaned address book accounts + accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)) + .map { LocalAddressBook(context, it, null) } + .forEach { + try { + if (!accountNames.contains(it.mainAccount.name)) + it.delete() + } catch(e: Exception) { + Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e) + } + } + // delete orphaned services in DB val db = EntryPointAccessors.fromApplication(context, AccountsUpdatedListenerEntryPoint::class.java).appDatabase() val serviceDao = db.serviceDao() - if (mainAccountNames.isEmpty()) + if (accountNames.isEmpty()) serviceDao.deleteAll() else - serviceDao.deleteExceptAccounts(mainAccountNames.toTypedArray()) + serviceDao.deleteExceptAccounts(accountNames.toTypedArray()) } - } \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt index 26c7d9d3c..e3405ac07 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt @@ -47,9 +47,17 @@ class EeloAccountAuthenticatorService : Service(), OnAccountsUpdateListener { Logger.log.info("Cleaning up orphaned accounts") val accountManager = AccountManager.get(context) - val accountNames = - accountManager.getAccountsByType(context.getString(R.string.account_type)) - .map { it.name } + + val accountNames = HashSet() + + val accounts = ArrayList() + accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)).forEach { accounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.google_account_type)).forEach { accounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type)).forEach { accounts.add(it) } + + for (account in accounts.toTypedArray()) { + accountNames += account.name + } // delete orphaned address book accounts accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloCalendarsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloCalendarsSyncAdapterService.kt new file mode 100644 index 000000000..26e1a9f04 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloCalendarsSyncAdapterService.kt @@ -0,0 +1,192 @@ +/* + * Copyright ECORP SAS 2022 + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 at.bitfire.davdroid.syncadapter + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.Context +import android.content.SyncResult +import android.os.AsyncTask +import android.os.Bundle +import android.provider.CalendarContract +import at.bitfire.davdroid.HttpClient +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Credentials +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.resource.LocalCalendar +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.ical4android.AndroidCalendar +import net.openid.appauth.AuthorizationService +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import java.util.logging.Level + +class EeloCalendarsSyncAdapterService : SyncAdapterService() { + + override fun syncAdapter() = CalendarsSyncAdapter(this, appDatabase) + + class CalendarsSyncAdapter( + context: Context, + appDatabase: AppDatabase + ) : SyncAdapter(context, appDatabase) { + override fun sync( + account: Account, + extras: Bundle, + authority: String, + httpClient: Lazy, + provider: ContentProviderClient, + syncResult: SyncResult + ) { + try { + val accountSettings = AccountSettings(context, account) + + /* don't run sync if + - sync conditions (e.g. "sync only in WiFi") are not met AND + - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions) + */ + if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions( + accountSettings + ) + ) + return + + if (accountSettings.getEventColors()) + AndroidCalendar.insertColors(provider, account) + else + AndroidCalendar.removeColors(provider, account) + + updateLocalCalendars(provider, account, accountSettings) + + val priorityCalendars = priorityCollections(extras) + val calendars = AndroidCalendar + .find( + account, + provider, + LocalCalendar.Factory, + "${CalendarContract.Calendars.SYNC_EVENTS}!=0", + null + ) + .sortedByDescending { priorityCalendars.contains(it.id) } + for (calendar in calendars) { + Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}") + CalendarSyncManager( + context, + account, + accountSettings, + extras, + httpClient.value, + authority, + syncResult, + calendar + ).let { + val authState = accountSettings.credentials().authState + if (authState != null) { + if (authState.needsTokenRefresh) { + val tokenRequest = authState.createTokenRefreshRequest() + + AuthorizationService(context).performTokenRequest(tokenRequest) { tokenResponse, ex -> + authState.update(tokenResponse, ex) + accountSettings.credentials( + Credentials( + account.name, + null, + authState, + null + ) + ) + it.accountSettings.credentials( + Credentials( + it.account.name, + null, + authState, + null + ) + ) + object : AsyncTask() { + override fun doInBackground(vararg params: Void): Void? { + it.performSync() + return null + } + }.execute() + } + } else { + it.performSync() + } + } else { + it.performSync() + } + } + } + } catch (e: Exception) { + Logger.log.log(Level.SEVERE, "Couldn't sync calendars", e) + } + Logger.log.info("Calendar sync complete") + } + + private fun updateLocalCalendars( + provider: ContentProviderClient, + account: Account, + settings: AccountSettings + ) { + val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV) + + val remoteCalendars = mutableMapOf() + + if (service != null) { + for (collection in db.collectionDao().getSyncCalendars(service.id)) { + remoteCalendars[collection.url] = collection + } + } + + // delete/update local calendars + val updateColors = settings.getManageCalendarColors() + for (calendar in AndroidCalendar.find( + account, + provider, + LocalCalendar.Factory, + null, + null + )) + calendar.name?.let { + val url = it.toHttpUrlOrNull()!! + val info = remoteCalendars[url] + + if (info == null) { + Logger.log.log(Level.INFO, "Deleting obsolete local calendar", url) + calendar.delete() + } else { + // remote CollectionInfo found for this local collection, update data + Logger.log.log(Level.FINE, "Updating local calendar $url", info) + calendar.update(info, updateColors) + // we already have a local calendar for this remote collection, don't take into consideration anymore + remoteCalendars -= url + } + } + + // create new local calendars + for ((_, info) in remoteCalendars) { + Logger.log.log(Level.INFO, "Adding local calendar", info) + LocalCalendar.create(account, provider, info) + } + } + + } +} + diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloTasksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloTasksSyncAdapterService.kt new file mode 100644 index 000000000..2dbbd4c80 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloTasksSyncAdapterService.kt @@ -0,0 +1,203 @@ +/* + * Copyright ECORP SAS 2022 + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 at.bitfire.davdroid.syncadapter + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.Context +import android.content.SyncResult +import android.os.AsyncTask +import android.os.Build +import android.os.Bundle +import at.bitfire.davdroid.HttpClient +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Credentials +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.resource.LocalTaskList +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.ical4android.AndroidTaskList +import at.bitfire.ical4android.TaskProvider +import net.openid.appauth.AuthorizationService +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import org.dmfs.tasks.contract.TaskContract +import java.util.logging.Level + + +/** + * Synchronization manager for CalDAV collections; handles tasks ({@code VTODO}). + */ +class EeloTasksSyncAdapterService : SyncAdapterService() { + + override fun syncAdapter() = TasksSyncAdapter(this, appDatabase) + + + class TasksSyncAdapter( + context: Context, + appDatabase: AppDatabase + ) : SyncAdapter(context, appDatabase) { + + override fun sync( + account: Account, + extras: Bundle, + authority: String, + httpClient: Lazy, + provider: ContentProviderClient, + syncResult: SyncResult + ) { + try { + val providerName = TaskProvider.ProviderName.fromAuthority(authority) + val taskProvider = TaskProvider.fromProviderClient(context, providerName, provider) + + // make sure account can be seen by OpenTasks + if (Build.VERSION.SDK_INT >= 26) + AccountManager.get(context).setAccountVisibility( + account, + taskProvider.name.packageName, + AccountManager.VISIBILITY_VISIBLE + ) + + val accountSettings = AccountSettings(context, account) + /* don't run sync if + - sync conditions (e.g. "sync only in WiFi") are not met AND + - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions) + */ + if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions( + accountSettings + ) + ) + return + + updateLocalTaskLists(taskProvider, account, accountSettings) + + val priorityTaskLists = priorityCollections(extras) + val taskLists = AndroidTaskList + .find( + account, + taskProvider, + LocalTaskList.Factory, + "${TaskContract.TaskLists.SYNC_ENABLED}!=0", + null + ) + .sortedByDescending { priorityTaskLists.contains(it.id) } + for (taskList in taskLists) { + Logger.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]") + TasksSyncManager( + context, + account, + accountSettings, + httpClient.value, + extras, + authority, + syncResult, + taskList + ).let { + val authState = accountSettings.credentials().authState + if (authState != null) { + if (authState.needsTokenRefresh) { + val tokenRequest = authState.createTokenRefreshRequest() + + AuthorizationService(context).performTokenRequest(tokenRequest, + AuthorizationService.TokenResponseCallback { tokenResponse, ex -> + authState.update(tokenResponse, ex) + accountSettings.credentials( + Credentials( + account.name, + null, + authState, + null + ) + ) + it.accountSettings.credentials( + Credentials( + it.account.name, + null, + authState, + null + ) + ) + object : AsyncTask() { + override fun doInBackground(vararg params: Void): Void? { + it.performSync() + return null + } + }.execute() + }) + } else { + it.performSync() + } + } else { + it.performSync() + } + } + } + } catch (e: TaskProvider.ProviderTooOldException) { + SyncUtils.notifyProviderTooOld(context, e) + syncResult.databaseError = true + } catch (e: Exception) { + Logger.log.log(Level.SEVERE, "Couldn't sync task lists", e) + syncResult.databaseError = true + } + + Logger.log.info("Task sync complete") + } + + private fun updateLocalTaskLists( + provider: TaskProvider, + account: Account, + settings: AccountSettings + ) { + val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV) + + val remoteTaskLists = mutableMapOf() + if (service != null) + for (collection in db.collectionDao().getSyncTaskLists(service.id)) { + remoteTaskLists[collection.url] = collection + } + + // delete/update local task lists + val updateColors = settings.getManageCalendarColors() + + for (list in AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null)) + list.syncId?.let { + val url = it.toHttpUrlOrNull()!! + val info = remoteTaskLists[url] + if (info == null) { + Logger.log.fine("Deleting obsolete local task list $url") + list.delete() + } else { + // remote CollectionInfo found for this local collection, update data + Logger.log.log(Level.FINE, "Updating local task list $url", info) + list.update(info, updateColors) + // we already have a local task list for this remote collection, don't take into consideration anymore + remoteTaskLists -= url + } + } + + // create new local task lists + for ((_, info) in remoteTaskLists) { + Logger.log.log(Level.INFO, "Adding local task list", info) + LocalTaskList.create(account, provider, info) + } + } + + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt index 7a8c35c37..e4b86b9e2 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt @@ -46,9 +46,16 @@ class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener { Logger.log.info("Cleaning up orphaned accounts") val accountManager = AccountManager.get(context) - val accountNames = - accountManager.getAccountsByType(context.getString(R.string.account_type)) - .map { it.name } + + val accountNames = HashSet() + val accounts = ArrayList() + accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)).forEach { accounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.google_account_type)).forEach { accounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type)).forEach { accounts.add(it) } + + for (account in accounts.toTypedArray()) { + accountNames += account.name + } // delete orphaned address book accounts accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleCalendarsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleCalendarsSyncAdapterService.kt new file mode 100644 index 000000000..643a649cd --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleCalendarsSyncAdapterService.kt @@ -0,0 +1,190 @@ +/* + * Copyright ECORP SAS 2022 + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 at.bitfire.davdroid.syncadapter + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.Context +import android.content.SyncResult +import android.os.AsyncTask +import android.os.Bundle +import android.provider.CalendarContract +import at.bitfire.davdroid.HttpClient +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Credentials +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.resource.LocalCalendar +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.ical4android.AndroidCalendar +import net.openid.appauth.AuthorizationService +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import java.util.logging.Level + +class GoogleCalendarsSyncAdapterService : SyncAdapterService() { + + override fun syncAdapter() = CalendarsSyncAdapter(this, appDatabase) + + + class CalendarsSyncAdapter( + context: Context, + db: AppDatabase + ) : SyncAdapter(context, db) { + + override fun sync( + account: Account, + extras: Bundle, + authority: String, + httpClient: Lazy, + provider: ContentProviderClient, + syncResult: SyncResult + ) { + try { + val accountSettings = AccountSettings(context, account) + + /* don't run sync if + - sync conditions (e.g. "sync only in WiFi") are not met AND + - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions) + */ + if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions( + accountSettings + ) + ) + return + + if (accountSettings.getEventColors()) + AndroidCalendar.insertColors(provider, account) + else + AndroidCalendar.removeColors(provider, account) + + updateLocalCalendars(provider, account, accountSettings) + + val priorityCalendars = priorityCollections(extras) + val calendars = AndroidCalendar + .find( + account, + provider, + LocalCalendar.Factory, + "${CalendarContract.Calendars.SYNC_EVENTS}!=0", + null + ) + .sortedByDescending { priorityCalendars.contains(it.id) } + for (calendar in calendars) { + Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}") + CalendarSyncManager( + context, + account, + accountSettings, + extras, + httpClient.value, + authority, + syncResult, + calendar + ).let { + val authState = accountSettings.credentials().authState + if (authState != null) { + if (authState.needsTokenRefresh) { + val tokenRequest = authState.createTokenRefreshRequest() + + AuthorizationService(context).performTokenRequest(tokenRequest, + AuthorizationService.TokenResponseCallback { tokenResponse, ex -> + authState.update(tokenResponse, ex) + accountSettings.credentials( + Credentials( + account.name, + null, + authState, + null + ) + ) + it.accountSettings.credentials( + Credentials( + it.account.name, + null, + authState, + null + ) + ) + object : AsyncTask() { + override fun doInBackground(vararg params: Void): Void? { + it.performSync() + return null + } + }.execute() + }) + } else { + it.performSync() + } + } else { + it.performSync() + } + } + } + } catch (e: Exception) { + Logger.log.log(Level.SEVERE, "Couldn't sync calendars", e) + } + Logger.log.info("Calendar sync complete") + } + + private fun updateLocalCalendars( + provider: ContentProviderClient, + account: Account, + settings: AccountSettings + ) { + val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV) + + val remoteCalendars = mutableMapOf() + if (service != null) + for (collection in db.collectionDao().getSyncCalendars(service.id)) { + remoteCalendars[collection.url] = collection + } + + // delete/update local calendars + val updateColors = settings.getManageCalendarColors() + for (calendar in AndroidCalendar.find( + account, + provider, + LocalCalendar.Factory, + null, + null + )) + calendar.name?.let { + val url = it.toHttpUrlOrNull()!! + val info = remoteCalendars[url] + if (info == null) { + Logger.log.log(Level.INFO, "Deleting obsolete local calendar", url) + calendar.delete() + } else { + // remote CollectionInfo found for this local collection, update data + Logger.log.log(Level.FINE, "Updating local calendar $url", info) + calendar.update(info, updateColors) + // we already have a local calendar for this remote collection, don't take into consideration anymore + remoteCalendars -= url + } + } + + // create new local calendars + for ((_, info) in remoteCalendars) { + Logger.log.log(Level.INFO, "Adding local calendar", info) + LocalCalendar.create(account, provider, info) + } + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleTasksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleTasksSyncAdapterService.kt new file mode 100644 index 000000000..8a3d0713f --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleTasksSyncAdapterService.kt @@ -0,0 +1,201 @@ +/* + * Copyright ECORP SAS 2022 + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 at.bitfire.davdroid.syncadapter + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.Context +import android.content.SyncResult +import android.os.AsyncTask +import android.os.Build +import android.os.Bundle +import at.bitfire.davdroid.HttpClient +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Credentials +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.resource.LocalTaskList +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.ical4android.AndroidTaskList +import at.bitfire.ical4android.TaskProvider +import net.openid.appauth.AuthorizationService +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import org.dmfs.tasks.contract.TaskContract +import java.util.logging.Level + +/** + * Synchronization manager for CalDAV collections; handles tasks ({@code VTODO}). + */ +class GoogleTasksSyncAdapterService : SyncAdapterService() { + + override fun syncAdapter() = TasksSyncAdapter(this, appDatabase) + + + class TasksSyncAdapter( + context: Context, + db: AppDatabase + ) : SyncAdapter(context, db) { + + override fun sync( + account: Account, + extras: Bundle, + authority: String, + httpClient: Lazy, + provider: ContentProviderClient, + syncResult: SyncResult + ) { + try { + val providerName = TaskProvider.ProviderName.fromAuthority(authority) + val taskProvider = TaskProvider.fromProviderClient(context, providerName, provider) + + // make sure account can be seen by OpenTasks + if (Build.VERSION.SDK_INT >= 26) + AccountManager.get(context).setAccountVisibility( + account, + taskProvider.name.packageName, + AccountManager.VISIBILITY_VISIBLE + ) + + val accountSettings = AccountSettings(context, account) + /* don't run sync if + - sync conditions (e.g. "sync only in WiFi") are not met AND + - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions) + */ + if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions( + accountSettings + ) + ) + return + + updateLocalTaskLists(taskProvider, account, accountSettings) + + val priorityTaskLists = priorityCollections(extras) + val taskLists = AndroidTaskList + .find( + account, + taskProvider, + LocalTaskList.Factory, + "${TaskContract.TaskLists.SYNC_ENABLED}!=0", + null + ) + .sortedByDescending { priorityTaskLists.contains(it.id) } + for (taskList in taskLists) { + Logger.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]") + TasksSyncManager( + context, + account, + accountSettings, + httpClient.value, + extras, + authority, + syncResult, + taskList + ).let { + val authState = accountSettings.credentials().authState + if (authState != null) { + if (authState.needsTokenRefresh) { + val tokenRequest = authState.createTokenRefreshRequest() + + AuthorizationService(context).performTokenRequest(tokenRequest, + AuthorizationService.TokenResponseCallback { tokenResponse, ex -> + authState.update(tokenResponse, ex) + accountSettings.credentials( + Credentials( + account.name, + null, + authState, + null + ) + ) + it.accountSettings.credentials( + Credentials( + it.account.name, + null, + authState, + null + ) + ) + object : AsyncTask() { + override fun doInBackground(vararg params: Void): Void? { + it.performSync() + return null + } + }.execute() + }) + } else { + it.performSync() + } + } else { + it.performSync() + } + } + } + } catch (e: TaskProvider.ProviderTooOldException) { + SyncUtils.notifyProviderTooOld(context, e) + syncResult.databaseError = true + } catch (e: Exception) { + Logger.log.log(Level.SEVERE, "Couldn't sync task lists", e) + syncResult.databaseError = true + } + + Logger.log.info("Task sync complete") + } + + private fun updateLocalTaskLists( + provider: TaskProvider, + account: Account, + settings: AccountSettings + ) { + val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV) + + val remoteTaskLists = mutableMapOf() + if (service != null) + for (collection in db.collectionDao().getSyncTaskLists(service.id)) { + remoteTaskLists[collection.url] = collection + } + + // delete/update local task lists + val updateColors = settings.getManageCalendarColors() + + for (list in AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null)) + list.syncId?.let { + val url = it.toHttpUrlOrNull()!! + val info = remoteTaskLists[url] + if (info == null) { + Logger.log.fine("Deleting obsolete local task list $url") + list.delete() + } else { + // remote CollectionInfo found for this local collection, update data + Logger.log.log(Level.FINE, "Updating local task list $url", info) + list.update(info, updateColors) + // we already have a local task list for this remote collection, don't take into consideration anymore + remoteTaskLists -= url + } + } + + // create new local task lists + for ((_, info) in remoteTaskLists) { + Logger.log.log(Level.INFO, "Adding local task list", info) + LocalTaskList.create(account, provider, info) + } + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt index fafd7c0c4..c9e9bc5ab 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt @@ -105,7 +105,7 @@ object SyncUtils { // check all accounts and (de)activate task provider(s) if a CalDAV service is defined val db = EntryPointAccessors.fromApplication(context, SyncUtilsEntryPoint::class.java).appDatabase() val accountManager = AccountManager.get(context) - for (account in accountManager.getAccountsByType(context.getString(R.string.account_type))) { + for (account in accountManager.accounts) { val hasCalDAV = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV) != null for (providerName in TaskProvider.ProviderName.values()) { val isSyncable = ContentResolver.getIsSyncable(account, providerName.authority) // may be -1 (unknown state) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.kt index 68ad8d06a..bb02ce57f 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.kt @@ -50,7 +50,7 @@ class TasksSyncManager( override fun prepare(): Boolean { collectionURL = (localCollection.syncId ?: return false).toHttpUrlOrNull() ?: return false - davCollection = DavCalendar(httpClient.okHttpClient, collectionURL, accountSettings.credentials().accessToken) + davCollection = DavCalendar(httpClient.okHttpClient, collectionURL, accountSettings.credentials().authState?.accessToken) return true } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt index a1fe1f1ae..253ac1944 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt @@ -255,9 +255,14 @@ class AccountListFragment: Fragment() { val context = getApplication() val collator = Collator.getInstance() - val sortedAccounts = accountManager - .getAccountsByType(context.getString(R.string.account_type)) - .sortedArrayWith { a, b -> + val accountsFromManager = ArrayList() + val accountManager = AccountManager.get(context) + accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)).forEach { accountsFromManager.add(it) } + accountManager.getAccountsByType(context.getString(R.string.google_account_type)).forEach { accountsFromManager.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type)).forEach { accountsFromManager.add(it) } + + val sortedAccounts = accountsFromManager + .sortedWith { a, b -> collator.compare(a.name, b.name) } val accountsWithInfo = sortedAccounts.map { account -> diff --git a/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.kt index 8573047d7..e87dec970 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.kt @@ -451,7 +451,11 @@ class DebugInfoActivity: AppCompatActivity() { writer.append("\nACCOUNTS\n\n") // main accounts val accountManager = AccountManager.get(context) - val mainAccounts = accountManager.getAccountsByType(context.getString(R.string.account_type)) + val mainAccounts = ArrayList() + accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)).forEach { mainAccounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.google_account_type)).forEach { mainAccounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type)).forEach { mainAccounts.add(it) } + val addressBookAccounts = accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).toMutableList() for (account in mainAccounts) { dumpMainAccount(account, writer) diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 675232ea1..3995579b5 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -32,7 +32,6 @@ import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.TaskUtils import at.bitfire.davdroid.settings.AccountSettings -import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.ui.account.AccountActivity @@ -158,7 +157,19 @@ class AccountDetailsFragment : Fragment() { fun createAccount(name: String, credentials: Credentials?, config: DavResourceFinder.Configuration, groupMethod: GroupMethod): LiveData { val result = MutableLiveData() viewModelScope.launch(Dispatchers.Default + NonCancellable) { - val account = Account(name, context.getString(R.string.account_type)) + var accountType = context.getString(R.string.account_type) + + val intent = Intent(context, LoginActivity::class.java) + when (intent.getStringExtra("SETUP_ACCOUNT_PROVIDER_TYPE")) { + LoginActivity.ACCOUNT_PROVIDER_EELO -> { + accountType = context.getString(R.string.eelo_account_type) + } + LoginActivity.ACCOUNT_PROVIDER_GOOGLE -> { + accountType = context.getString(R.string.google_account_type) + } + } + + val account = Account(name, accountType) // create Android account val userData = AccountSettings.initialUserData(credentials) @@ -181,7 +192,13 @@ class AccountDetailsFragment : Fragment() { val addrBookAuthority = context.getString(R.string.address_books_authority) if (config.cardDAV != null) { // insert CardDAV service - val id = insertService(name, credentials?.authState?.jsonSerializeString(), Service.TYPE_CARDDAV, config.cardDAV) + val id = insertService( + name, + credentials?.authState?.jsonSerializeString(), + accountType, + Service.TYPE_CARDDAV, + config.cardDAV + ) // initial CardDAV account settings accountSettings.setGroupMethod(groupMethod) @@ -198,7 +215,13 @@ class AccountDetailsFragment : Fragment() { if (config.calDAV != null) { // insert CalDAV service - val id = insertService(name, credentials?.authState?.jsonSerializeString(), Service.TYPE_CALDAV, config.calDAV) + val id = insertService( + name, + credentials?.authState?.jsonSerializeString(), + accountType, + Service.TYPE_CALDAV, + config.calDAV + ) // start CalDAV service detection (refresh collections) refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id) @@ -227,9 +250,15 @@ class AccountDetailsFragment : Fragment() { return result } - private fun insertService(accountName: String, authState: String?, type: String, info: DavResourceFinder.Configuration.ServiceInfo): Long { + private fun insertService( + accountName: String, + authState: String?, + accountType: String, + type: String, + info: DavResourceFinder.Configuration.ServiceInfo + ): Long { // insert service - val service = Service(0, accountName, authState, type, info.principal) + val service = Service(0, accountName, authState, accountType, type, info.principal) val serviceId = db.serviceDao().insertOrReplace(service) // insert home sets diff --git a/app/src/main/res/xml/eelo_sync_calendars.xml b/app/src/main/res/xml/eelo_sync_calendars.xml new file mode 100644 index 000000000..27d291ea3 --- /dev/null +++ b/app/src/main/res/xml/eelo_sync_calendars.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/xml/eelo_sync_tasks.xml b/app/src/main/res/xml/eelo_sync_tasks.xml new file mode 100644 index 000000000..7c54fb19d --- /dev/null +++ b/app/src/main/res/xml/eelo_sync_tasks.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/xml/google_sync_calendars.xml b/app/src/main/res/xml/google_sync_calendars.xml new file mode 100644 index 000000000..e61c04cb9 --- /dev/null +++ b/app/src/main/res/xml/google_sync_calendars.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/xml/google_sync_tasks.xml b/app/src/main/res/xml/google_sync_tasks.xml new file mode 100644 index 000000000..8c1b63c5e --- /dev/null +++ b/app/src/main/res/xml/google_sync_tasks.xml @@ -0,0 +1,15 @@ + + + + -- GitLab From 0637918eef2f16760de08ea1a7e65f6e93c3468f Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Wed, 11 Jul 2018 22:51:09 +0530 Subject: [PATCH 036/285] Show eelo and Google calendar accounts in their own custom account types --- app/src/main/AndroidManifest.xml | 70 ++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 480ac9c1c..a6d19ce0d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -262,9 +262,9 @@ android:resource="@xml/contacts"/> - + @@ -272,12 +272,40 @@ + android:resource="@xml/eelo_account_authenticator" /> + + + + + + + + - + + + + + + + + + @@ -285,9 +313,37 @@ + android:resource="@xml/google_account_authenticator" /> - + + + + + + + + + + + + + + + + + -- GitLab From 6a24c0f6960be505abccc61116c02cda53f549e9 Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Thu, 12 Jul 2018 00:16:30 +0530 Subject: [PATCH 037/285] Show eelo and Google contacts accounts in their own custom account types --- app/src/main/AndroidManifest.xml | 88 ++++++++++ .../at/bitfire/davdroid/db/AppDatabase.kt | 3 +- .../java/at/bitfire/davdroid/db/Service.kt | 1 + .../java/at/bitfire/davdroid/db/ServiceDao.kt | 4 + .../davdroid/resource/LocalAddressBook.kt | 32 ++-- .../syncadapter/AccountsUpdatedListener.kt | 7 +- .../AddressBooksSyncAdapterService.kt | 2 +- .../EeloAccountAuthenticatorService.kt | 7 +- .../EeloAddressBooksSyncAdapterService.kt | 164 ++++++++++++++++++ .../EeloContactsSyncAdapterService.kt | 95 ++++++++++ .../EeloNullAuthenticatorService.kt | 61 +++++++ .../GoogleAccountAuthenticatorService.kt | 7 +- .../GoogleAddressBooksSyncAdapterService.kt | 164 ++++++++++++++++++ .../GoogleContactsSyncAdapterService.kt | 118 +++++++++++++ .../GoogleNullAuthenticatorService.kt | 61 +++++++ .../ui/setup/AccountDetailsFragment.kt | 8 +- app/src/main/res/values/strings.xml | 4 + ...ccount_authenticator_eelo_address_book.xml | 15 ++ ...ount_authenticator_google_address_book.xml | 15 ++ .../main/res/xml/eelo_sync_address_books.xml | 14 ++ app/src/main/res/xml/eelo_sync_contacts.xml | 15 ++ .../res/xml/google_sync_address_books.xml | 14 ++ app/src/main/res/xml/google_sync_contacts.xml | 15 ++ 23 files changed, 892 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAddressBooksSyncAdapterService.kt create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/EeloContactsSyncAdapterService.kt create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/EeloNullAuthenticatorService.kt create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAddressBooksSyncAdapterService.kt create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleContactsSyncAdapterService.kt create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleNullAuthenticatorService.kt create mode 100644 app/src/main/res/xml/account_authenticator_eelo_address_book.xml create mode 100644 app/src/main/res/xml/account_authenticator_google_address_book.xml create mode 100644 app/src/main/res/xml/eelo_sync_address_books.xml create mode 100644 app/src/main/res/xml/eelo_sync_contacts.xml create mode 100644 app/src/main/res/xml/google_sync_address_books.xml create mode 100644 app/src/main/res/xml/google_sync_contacts.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a6d19ce0d..95b0eed02 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -302,6 +302,50 @@ android:name="android.content.SyncAdapter" android:resource="@xml/eelo_sync_tasks" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt b/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt index 31475b71a..e582d1bc8 100644 --- a/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt +++ b/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt @@ -118,11 +118,12 @@ abstract class AppDatabase: RoomDatabase() { "accountName TEXT NOT NULL," + "authState TEXT," + "accountType TEXT," + + "addressBookAccountType TEXT," + "type TEXT NOT NULL," + "principal TEXT DEFAULT NULL" + ")", "CREATE UNIQUE INDEX index_service_accountName_type ON service(accountName, type)", - "INSERT INTO service(id, accountName, authState, accountType, type, principal) SELECT _id, accountName, authState, accountType, service, principal FROM services", + "INSERT INTO service(id, accountName, authState, accountType, addressBookAccountType, type, principal) SELECT _id, accountName, authState, accountType, addressBookAccountType, service, principal FROM services", "DROP TABLE services", // migrate "homesets" to "homeset": rename columns, make id NOT NULL diff --git a/app/src/main/java/at/bitfire/davdroid/db/Service.kt b/app/src/main/java/at/bitfire/davdroid/db/Service.kt index ca024482b..0c22ae4db 100644 --- a/app/src/main/java/at/bitfire/davdroid/db/Service.kt +++ b/app/src/main/java/at/bitfire/davdroid/db/Service.kt @@ -22,6 +22,7 @@ data class Service( var authState: String?, var accountType: String, + var addressBookAccountType: String, var type: String, diff --git a/app/src/main/java/at/bitfire/davdroid/db/ServiceDao.kt b/app/src/main/java/at/bitfire/davdroid/db/ServiceDao.kt index 62ed226c9..d45bb779d 100644 --- a/app/src/main/java/at/bitfire/davdroid/db/ServiceDao.kt +++ b/app/src/main/java/at/bitfire/davdroid/db/ServiceDao.kt @@ -22,6 +22,10 @@ interface ServiceDao { @Query("SELECT * FROM service WHERE id=:id") fun get(id: Long): Service? + + @Query("SELECT * FROM service WHERE accountName=:accountName") + fun getByAccountName(accountName: String): Service? + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertOrReplace(service: Service): Long diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.kt b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.kt index dfeb7e727..660d290b1 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.kt +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.kt @@ -16,6 +16,7 @@ import android.provider.ContactsContract.RawContacts import android.util.Base64 import at.bitfire.davdroid.DavUtils import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.SyncState import at.bitfire.davdroid.log.Logger @@ -46,8 +47,10 @@ open class LocalAddressBook( const val USER_DATA_URL = "url" const val USER_DATA_READ_ONLY = "read_only" - fun create(context: Context, provider: ContentProviderClient, mainAccount: Account, info: Collection): LocalAddressBook { - val account = Account(accountName(mainAccount, info), context.getString(R.string.account_type_address_book)) + fun create(context: Context, db: AppDatabase, provider: ContentProviderClient, mainAccount: Account, info: Collection): LocalAddressBook { + val service = db.serviceDao().getByAccountName(mainAccount.name) ?: throw IllegalArgumentException("Service not found") + val account = Account(accountName(mainAccount, info), service.addressBookAccountType) + val userData = initialUserData(mainAccount, info.url.toString()) Logger.log.log(Level.INFO, "Creating local address book $account", userData) if (!AccountUtils.createAccount(context, account, userData)) @@ -65,18 +68,18 @@ open class LocalAddressBook( return addressBook } + + fun findAll(context: Context, provider: ContentProviderClient?, mainAccount: Account?): List { + val accountManager = AccountManager.get(context) + val accounts = ArrayList() + accountManager.getAccountsByType(context.getString(R.string.account_type_eelo_address_book)).forEach { accounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type_google_address_book)).forEach { accounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).forEach { accounts.add(it) } - fun findAll(context: Context, provider: ContentProviderClient?, mainAccount: Account) = AccountManager.get(context) - .getAccountsByType(context.getString(R.string.account_type_address_book)) - .map { LocalAddressBook(context, it, provider) } - .filter { - try { - it.mainAccount == mainAccount - } catch(e: IllegalStateException) { - false - } - } + return accounts.toTypedArray().map { LocalAddressBook(context, it, provider) } + .filter { mainAccount == null || it.mainAccount == mainAccount } .toList() + } fun accountName(mainAccount: Account, info: Collection): String { val baos = ByteArrayOutputStream() @@ -102,7 +105,10 @@ open class LocalAddressBook( } fun mainAccount(context: Context, account: Account): Account = - if (account.type == context.getString(R.string.account_type_address_book)) { + if (account.type == context.getString(R.string.account_type_address_book) || + account.type == context.getString(R.string.account_type_eelo_address_book) || + account.type == context.getString(R.string.account_type_google_address_book)) { + val manager = AccountManager.get(context) val accountName = manager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME) val accountType = manager.getUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountsUpdatedListener.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountsUpdatedListener.kt index c2e85d2c7..d38040f60 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountsUpdatedListener.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountsUpdatedListener.kt @@ -83,8 +83,11 @@ class AccountsUpdatedListener private constructor( } // delete orphaned address book accounts - accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)) - .map { LocalAddressBook(context, it, null) } + val addressBookAccounts = ArrayList() + accountManager.getAccountsByType(context.getString(R.string.account_type_eelo_address_book)).forEach { addressBookAccounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type_google_address_book)).forEach { addressBookAccounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).forEach { addressBookAccounts.add(it) } + addressBookAccounts.map { LocalAddressBook(context, it, null) } .forEach { try { if (!accountNames.contains(it.mainAccount.name)) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt index 275ab782c..3996eb2e4 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt @@ -108,7 +108,7 @@ class AddressBooksSyncAdapterService : SyncAdapterService() { // create new local address books for ((_, info) in remoteAddressBooks) { Logger.log.log(Level.INFO, "Adding local address book", info) - LocalAddressBook.create(context, contactsProvider, account, info) + LocalAddressBook.create(context, db, contactsProvider, account, info) } } finally { contactsProvider?.closeCompat() diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt index e3405ac07..0ed098d3f 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt @@ -60,8 +60,11 @@ class EeloAccountAuthenticatorService : Service(), OnAccountsUpdateListener { } // delete orphaned address book accounts - accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)) - .map { LocalAddressBook(context, it, null) } + val addressBookAccounts = ArrayList() + accountManager.getAccountsByType(context.getString(R.string.account_type_eelo_address_book)).forEach { addressBookAccounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type_google_address_book)).forEach { addressBookAccounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).forEach { addressBookAccounts.add(it) } + addressBookAccounts.map { LocalAddressBook(context, it, null) } .forEach { try { if (!accountNames.contains(it.mainAccount.name)) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAddressBooksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAddressBooksSyncAdapterService.kt new file mode 100644 index 000000000..087731a9d --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAddressBooksSyncAdapterService.kt @@ -0,0 +1,164 @@ +/* + * Copyright ECORP SAS 2022 + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 at.bitfire.davdroid.syncadapter + +import android.Manifest +import android.accounts.Account +import android.content.* +import android.content.pm.PackageManager +import android.os.Bundle +import android.provider.ContactsContract +import androidx.core.content.ContextCompat +import at.bitfire.davdroid.HttpClient +import at.bitfire.davdroid.PermissionUtils +import at.bitfire.davdroid.closeCompat +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.ui.account.AccountActivity +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import java.util.logging.Level + +class EeloAddressBooksSyncAdapterService : SyncAdapterService() { + + override fun syncAdapter() = AddressBooksSyncAdapter(this, appDatabase) + + class AddressBooksSyncAdapter( + context: Context, + db: AppDatabase + ) : SyncAdapter(context, db) { + + override fun sync( + account: Account, + extras: Bundle, + authority: String, + httpClient: Lazy, + provider: ContentProviderClient, + syncResult: SyncResult + ) { + try { + val accountSettings = AccountSettings(context, account) + + /* don't run sync if + - sync conditions (e.g. "sync only in WiFi") are not met AND + - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions) + */ + if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions( + accountSettings + ) + ) + return + + if (updateLocalAddressBooks(account, syncResult)) + for (addressBookAccount in LocalAddressBook.findAll(context, null, account) + .map { it.account }) { + Logger.log.log( + Level.INFO, + "Running sync for address book", + addressBookAccount + ) + val syncExtras = Bundle(extras) + syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true) + syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true) + ContentResolver.requestSync( + addressBookAccount, + ContactsContract.AUTHORITY, + syncExtras + ) + } + } catch (e: Exception) { + Logger.log.log(Level.SEVERE, "Couldn't sync address books", e) + } + + Logger.log.info("Address book sync complete") + } + + private fun updateLocalAddressBooks(account: Account, syncResult: SyncResult): Boolean { + val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CARDDAV) + + val remoteAddressBooks = mutableMapOf() + if (service != null) + for (collection in db.collectionDao().getByServiceAndSync(service.id)) + remoteAddressBooks[collection.url] = collection + + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.WRITE_CONTACTS + ) != PackageManager.PERMISSION_GRANTED + ) { + if (remoteAddressBooks.isEmpty()) + Logger.log.info("No contacts permission, but no address book selected for synchronization") + else { + // no contacts permission, but address books should be synchronized -> show notification + val intent = Intent(context, AccountActivity::class.java) + intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + PermissionUtils.notifyPermissions(context, intent) + } + return false + } + + val contactsProvider = + context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY) + try { + if (contactsProvider == null) { + Logger.log.severe("Couldn't access contacts provider") + syncResult.databaseError = true + return false + } + + // delete/update local address books + for (addressBook in LocalAddressBook.findAll(context, contactsProvider, account)) { + val url = addressBook.url.toHttpUrlOrNull()!! + val info = remoteAddressBooks[url] + if (info == null) { + Logger.log.log(Level.INFO, "Deleting obsolete local address book", url) + addressBook.delete() + } else { + // remote CollectionInfo found for this local collection, update data + try { + Logger.log.log(Level.FINE, "Updating local address book $url", info) + addressBook.update(info) + } catch (e: Exception) { + Logger.log.log(Level.WARNING, "Couldn't rename address book account", e) + } + // we already have a local address book for this remote collection, don't take into consideration anymore + remoteAddressBooks -= url + } + } + + // create new local address books + for ((_, info) in remoteAddressBooks) { + Logger.log.log(Level.INFO, "Adding local address book", info) + LocalAddressBook.create(context, db, contactsProvider, account, info) + } + } finally { + contactsProvider?.closeCompat() + } + + return true + } + + } + +} + diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloContactsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloContactsSyncAdapterService.kt new file mode 100644 index 000000000..b5c0419d8 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloContactsSyncAdapterService.kt @@ -0,0 +1,95 @@ +/* + * Copyright ECORP SAS 2022 + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 at.bitfire.davdroid.syncadapter + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.Context +import android.content.SyncResult +import android.os.Bundle +import android.provider.ContactsContract +import at.bitfire.davdroid.HttpClient +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.settings.AccountSettings +import java.util.logging.Level + +class EeloContactsSyncAdapterService: SyncAdapterService() { + + companion object { + const val PREVIOUS_GROUP_METHOD = "previous_group_method" + } + + override fun syncAdapter() = ContactsSyncAdapter(this, appDatabase) + + + class ContactsSyncAdapter( + context: Context, + db: AppDatabase + ): SyncAdapter(context, db) { + + override fun sync( + account: Account, + extras: Bundle, + authority: String, + httpClient: Lazy, + provider: ContentProviderClient, + syncResult: SyncResult + ) { + try { + val addressBook = LocalAddressBook(context, account, provider) + val accountSettings = AccountSettings(context, addressBook.mainAccount) + + // handle group method change + val groupMethod = accountSettings.getGroupMethod().name + accountSettings.accountManager.getUserData(account, PREVIOUS_GROUP_METHOD)?.let { previousGroupMethod -> + if (previousGroupMethod != groupMethod) { + Logger.log.info("Group method changed, deleting all local contacts/groups") + + // delete all local contacts and groups so that they will be downloaded again + provider.delete(addressBook.syncAdapterURI(ContactsContract.RawContacts.CONTENT_URI), null, null) + provider.delete(addressBook.syncAdapterURI(ContactsContract.Groups.CONTENT_URI), null, null) + + // reset sync state + addressBook.syncState = null + } + } + accountSettings.accountManager.setUserData(account, PREVIOUS_GROUP_METHOD, groupMethod) + + /* don't run sync if + - sync conditions (e.g. "sync only in WiFi") are not met AND + - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions) + */ + if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings)) + return + + Logger.log.info("Synchronizing address book: ${addressBook.url}") + Logger.log.info("Taking settings from: ${addressBook.mainAccount}") + + ContactsSyncManager(context, account, accountSettings, httpClient.value, extras, authority, syncResult, provider, addressBook).let { + it.performSync() + } + } catch(e: Exception) { + Logger.log.log(Level.SEVERE, "Couldn't sync contacts", e) + } + Logger.log.info("Contacts sync complete") + } + } +} + diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloNullAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloNullAuthenticatorService.kt new file mode 100644 index 000000000..58367a45a --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloNullAuthenticatorService.kt @@ -0,0 +1,61 @@ +/* + * Copyright ECORP SAS 2022 + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 at.bitfire.davdroid.syncadapter + +import android.accounts.AbstractAccountAuthenticator +import android.accounts.Account +import android.accounts.AccountAuthenticatorResponse +import android.accounts.AccountManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Bundle +import at.bitfire.davdroid.ui.AccountsActivity + +class EeloNullAuthenticatorService: Service() { + + private lateinit var accountAuthenticator: AccountAuthenticator + + override fun onCreate() { + accountAuthenticator = AccountAuthenticator(this) + } + + override fun onBind(intent: Intent?) = + accountAuthenticator.iBinder.takeIf { intent?.action == AccountManager.ACTION_AUTHENTICATOR_INTENT } + + + private class AccountAuthenticator( + val context: Context + ): AbstractAccountAuthenticator(context) { + + override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array?, options: Bundle?): Bundle { + val intent = Intent(context, AccountsActivity::class.java) + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response) + val bundle = Bundle(1) + bundle.putParcelable(AccountManager.KEY_INTENT, intent) + return bundle + } + + override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = null + override fun getAuthTokenLabel(p0: String?) = null + override fun confirmCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Bundle?) = null + override fun updateCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null + override fun getAuthToken(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null + override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array?) = null + } +} + diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt index e4b86b9e2..eaf9af5e1 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt @@ -58,8 +58,11 @@ class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener { } // delete orphaned address book accounts - accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)) - .map { LocalAddressBook(context, it, null) } + val addressBookAccounts = ArrayList() + accountManager.getAccountsByType(context.getString(R.string.account_type_eelo_address_book)).forEach { addressBookAccounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type_google_address_book)).forEach { addressBookAccounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).forEach { addressBookAccounts.add(it) } + addressBookAccounts.map { LocalAddressBook(context, it, null) } .forEach { try { if (!accountNames.contains(it.mainAccount.name)) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAddressBooksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAddressBooksSyncAdapterService.kt new file mode 100644 index 000000000..128a26f66 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAddressBooksSyncAdapterService.kt @@ -0,0 +1,164 @@ +/* + * Copyright ECORP SAS 2022 + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 at.bitfire.davdroid.syncadapter + +import android.Manifest +import android.accounts.Account +import android.content.* +import android.content.pm.PackageManager +import android.os.Bundle +import android.provider.ContactsContract +import androidx.core.content.ContextCompat +import at.bitfire.davdroid.HttpClient +import at.bitfire.davdroid.PermissionUtils +import at.bitfire.davdroid.closeCompat +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.ui.account.AccountActivity +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import java.util.logging.Level + +class GoogleAddressBooksSyncAdapterService : SyncAdapterService() { + + override fun syncAdapter() = AddressBooksSyncAdapter(this, appDatabase) + + class AddressBooksSyncAdapter( + context: Context, + db: AppDatabase + ) : SyncAdapter(context, db) { + + override fun sync( + account: Account, + extras: Bundle, + authority: String, + httpClient: Lazy, + provider: ContentProviderClient, + syncResult: SyncResult + ) { + try { + val accountSettings = AccountSettings(context, account) + + /* don't run sync if + - sync conditions (e.g. "sync only in WiFi") are not met AND + - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions) + */ + if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions( + accountSettings + ) + ) + return + + if (updateLocalAddressBooks(account, syncResult)) + for (addressBookAccount in LocalAddressBook.findAll(context, null, account) + .map { it.account }) { + Logger.log.log( + Level.INFO, + "Running sync for address book", + addressBookAccount + ) + val syncExtras = Bundle(extras) + syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true) + syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true) + ContentResolver.requestSync( + addressBookAccount, + ContactsContract.AUTHORITY, + syncExtras + ) + } + } catch (e: Exception) { + Logger.log.log(Level.SEVERE, "Couldn't sync address books", e) + } + + Logger.log.info("Address book sync complete") + } + + private fun updateLocalAddressBooks(account: Account, syncResult: SyncResult): Boolean { + val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CARDDAV) + + val remoteAddressBooks = mutableMapOf() + if (service != null) + for (collection in db.collectionDao().getByServiceAndSync(service.id)) + remoteAddressBooks[collection.url] = collection + + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.WRITE_CONTACTS + ) != PackageManager.PERMISSION_GRANTED + ) { + if (remoteAddressBooks.isEmpty()) + Logger.log.info("No contacts permission, but no address book selected for synchronization") + else { + // no contacts permission, but address books should be synchronized -> show notification + val intent = Intent(context, AccountActivity::class.java) + intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + PermissionUtils.notifyPermissions(context, intent) + } + return false + } + + val contactsProvider = + context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY) + try { + if (contactsProvider == null) { + Logger.log.severe("Couldn't access contacts provider") + syncResult.databaseError = true + return false + } + + // delete/update local address books + for (addressBook in LocalAddressBook.findAll(context, contactsProvider, account)) { + val url = addressBook.url.toHttpUrlOrNull()!! + val info = remoteAddressBooks[url] + if (info == null) { + Logger.log.log(Level.INFO, "Deleting obsolete local address book", url) + addressBook.delete() + } else { + // remote CollectionInfo found for this local collection, update data + try { + Logger.log.log(Level.FINE, "Updating local address book $url", info) + addressBook.update(info) + } catch (e: Exception) { + Logger.log.log(Level.WARNING, "Couldn't rename address book account", e) + } + // we already have a local address book for this remote collection, don't take into consideration anymore + remoteAddressBooks -= url + } + } + + // create new local address books + for ((_, info) in remoteAddressBooks) { + Logger.log.log(Level.INFO, "Adding local address book", info) + LocalAddressBook.create(context, db, contactsProvider, account, info) + } + } finally { + contactsProvider?.closeCompat() + } + + return true + } + + } + +} + diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleContactsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleContactsSyncAdapterService.kt new file mode 100644 index 000000000..fd8c3ade1 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleContactsSyncAdapterService.kt @@ -0,0 +1,118 @@ +/* + * Copyright ECORP SAS 2022 + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 at.bitfire.davdroid.syncadapter + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.Context +import android.content.SyncResult +import android.os.Bundle +import android.provider.ContactsContract +import at.bitfire.davdroid.HttpClient +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.settings.AccountSettings +import java.util.logging.Level + +class GoogleContactsSyncAdapterService : SyncAdapterService() { + + companion object { + const val PREVIOUS_GROUP_METHOD = "previous_group_method" + } + + override fun syncAdapter() = ContactsSyncAdapter(this, appDatabase) + + class ContactsSyncAdapter( + context: Context, + db: AppDatabase + ) : SyncAdapter(context, db) { + + override fun sync( + account: Account, + extras: Bundle, + authority: String, + httpClient: Lazy, + provider: ContentProviderClient, + syncResult: SyncResult + ) { + try { + val addressBook = LocalAddressBook(context, account, provider) + val accountSettings = AccountSettings(context, addressBook.mainAccount) + + // handle group method change + val groupMethod = accountSettings.getGroupMethod().name + accountSettings.accountManager.getUserData(account, PREVIOUS_GROUP_METHOD) + ?.let { previousGroupMethod -> + if (previousGroupMethod != groupMethod) { + Logger.log.info("Group method changed, deleting all local contacts/groups") + + // delete all local contacts and groups so that they will be downloaded again + provider.delete( + addressBook.syncAdapterURI(ContactsContract.RawContacts.CONTENT_URI), + null, + null + ) + provider.delete( + addressBook.syncAdapterURI(ContactsContract.Groups.CONTENT_URI), + null, + null + ) + + // reset sync state + addressBook.syncState = null + } + } + accountSettings.accountManager.setUserData( + account, + PREVIOUS_GROUP_METHOD, + groupMethod + ) + + /* don't run sync if + - sync conditions (e.g. "sync only in WiFi") are not met AND + - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions) + */ + if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions( + accountSettings + ) + ) + return + + Logger.log.info("Synchronizing address book: ${addressBook.url}") + Logger.log.info("Taking settings from: ${addressBook.mainAccount}") + + ContactsSyncManager( + context, + account, + accountSettings, + httpClient.value, + extras, + authority, + syncResult, + provider, + addressBook + ).performSync() + } catch (e: Exception) { + Logger.log.log(Level.SEVERE, "Couldn't sync contacts", e) + } + Logger.log.info("Contacts sync complete") + } + } +} + diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleNullAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleNullAuthenticatorService.kt new file mode 100644 index 000000000..0485928f2 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleNullAuthenticatorService.kt @@ -0,0 +1,61 @@ +/* + * Copyright ECORP SAS 2022 + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 at.bitfire.davdroid.syncadapter + +import android.accounts.AbstractAccountAuthenticator +import android.accounts.Account +import android.accounts.AccountAuthenticatorResponse +import android.accounts.AccountManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Bundle +import at.bitfire.davdroid.ui.AccountsActivity + +class GoogleNullAuthenticatorService: Service() { + + private lateinit var accountAuthenticator: AccountAuthenticator + + override fun onCreate() { + accountAuthenticator = AccountAuthenticator(this) + } + + override fun onBind(intent: Intent?) = + accountAuthenticator.iBinder.takeIf { intent?.action == AccountManager.ACTION_AUTHENTICATOR_INTENT } + + + private class AccountAuthenticator( + val context: Context + ): AbstractAccountAuthenticator(context) { + + override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array?, options: Bundle?): Bundle { + val intent = Intent(context, AccountsActivity::class.java) + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response) + val bundle = Bundle(1) + bundle.putParcelable(AccountManager.KEY_INTENT, intent) + return bundle + } + + override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = null + override fun getAuthTokenLabel(p0: String?) = null + override fun confirmCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Bundle?) = null + override fun updateCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null + override fun getAuthToken(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null + override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array?) = null + } +} + diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 3995579b5..41e5f0dfc 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -158,14 +158,17 @@ class AccountDetailsFragment : Fragment() { val result = MutableLiveData() viewModelScope.launch(Dispatchers.Default + NonCancellable) { var accountType = context.getString(R.string.account_type) + var addressBookAccountType = context.getString(R.string.account_type_address_book) val intent = Intent(context, LoginActivity::class.java) when (intent.getStringExtra("SETUP_ACCOUNT_PROVIDER_TYPE")) { LoginActivity.ACCOUNT_PROVIDER_EELO -> { accountType = context.getString(R.string.eelo_account_type) + addressBookAccountType = context.getString(R.string.account_type_eelo_address_book) } LoginActivity.ACCOUNT_PROVIDER_GOOGLE -> { accountType = context.getString(R.string.google_account_type) + addressBookAccountType = context.getString(R.string.account_type_google_address_book) } } @@ -196,6 +199,7 @@ class AccountDetailsFragment : Fragment() { name, credentials?.authState?.jsonSerializeString(), accountType, + addressBookAccountType, Service.TYPE_CARDDAV, config.cardDAV ) @@ -219,6 +223,7 @@ class AccountDetailsFragment : Fragment() { name, credentials?.authState?.jsonSerializeString(), accountType, + addressBookAccountType, Service.TYPE_CALDAV, config.calDAV ) @@ -254,11 +259,12 @@ class AccountDetailsFragment : Fragment() { accountName: String, authState: String?, accountType: String, + addressBookAccountType: String, type: String, info: DavResourceFinder.Configuration.ServiceInfo ): Long { // insert service - val service = Service(0, accountName, authState, accountType, type, info.principal) + val service = Service(0, accountName, authState, accountType, addressBookAccountType, type, info.principal) val serviceId = db.serviceDao().insertOrReplace(service) // insert home sets diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ef0203a8d..03510b054 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,6 +13,10 @@ e.foundation.webdav.eelo foundation.e.accountmanager.address_book WebDav Address book + foundation.e.accountmanager.eelo.address_book + eelo Address book + foundation.e.accountmanager.google.address_book + Google Address book foundation.e.accountmanager.addressbooks Address books This field is required diff --git a/app/src/main/res/xml/account_authenticator_eelo_address_book.xml b/app/src/main/res/xml/account_authenticator_eelo_address_book.xml new file mode 100644 index 000000000..31168e6cb --- /dev/null +++ b/app/src/main/res/xml/account_authenticator_eelo_address_book.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/xml/account_authenticator_google_address_book.xml b/app/src/main/res/xml/account_authenticator_google_address_book.xml new file mode 100644 index 000000000..26ea075d5 --- /dev/null +++ b/app/src/main/res/xml/account_authenticator_google_address_book.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/xml/eelo_sync_address_books.xml b/app/src/main/res/xml/eelo_sync_address_books.xml new file mode 100644 index 000000000..845ea6c17 --- /dev/null +++ b/app/src/main/res/xml/eelo_sync_address_books.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/xml/eelo_sync_contacts.xml b/app/src/main/res/xml/eelo_sync_contacts.xml new file mode 100644 index 000000000..8b93ebf6e --- /dev/null +++ b/app/src/main/res/xml/eelo_sync_contacts.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/xml/google_sync_address_books.xml b/app/src/main/res/xml/google_sync_address_books.xml new file mode 100644 index 000000000..7bdbf6f90 --- /dev/null +++ b/app/src/main/res/xml/google_sync_address_books.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/xml/google_sync_contacts.xml b/app/src/main/res/xml/google_sync_contacts.xml new file mode 100644 index 000000000..b6e2633c5 --- /dev/null +++ b/app/src/main/res/xml/google_sync_contacts.xml @@ -0,0 +1,15 @@ + + + + -- GitLab From 9146f29e18bb833c50c3a11ea38abf4914b93bd1 Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Thu, 12 Jul 2018 10:16:02 +0530 Subject: [PATCH 038/285] Improve account configuration message, auto-add Google and eelo accounts --- .../ui/setup/AccountDetailsFragment.kt | 26 +++++++++++++++++++ .../ui/setup/DetectConfigurationFragment.kt | 1 + app/src/main/res/values/strings.xml | 5 ++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 41e5f0dfc..63803309a 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -6,6 +6,7 @@ package at.bitfire.davdroid.ui.setup import android.accounts.Account import android.accounts.AccountManager +import android.app.Activity import android.content.ContentResolver import android.content.Context import android.content.Intent @@ -16,6 +17,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels @@ -134,6 +136,30 @@ class AccountDetailsFragment : Fragment() { } else v.contactGroupMethod.isEnabled = true + if (activity!!.intent.getStringExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE) == LoginActivity.ACCOUNT_PROVIDER_EELO || + activity!!.intent.getStringExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE) == LoginActivity.ACCOUNT_PROVIDER_GOOGLE) { + val name = model.name.value + if (name.isNullOrBlank()) + model.nameError.value = getString(R.string.login_account_name_required) + else { + val idx = v.contactGroupMethod.selectedItemPosition + val groupMethodName = resources.getStringArray(R.array.settings_contact_group_method_values)[idx] + + model.createAccount( + name, + loginModel.credentials!!, + config, + GroupMethod.valueOf(groupMethodName) + ).observe(viewLifecycleOwner, Observer { success -> + if (success) { + Toast.makeText(context, R.string.message_account_added_successfully, Toast.LENGTH_LONG).show() + requireActivity().setResult(Activity.RESULT_OK) + requireActivity().finish() + } + }) + } + } + return v.root } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt index 48afdcfe5..1bb4e895d 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt @@ -112,6 +112,7 @@ class DetectConfigurationFragment: Fragment() { .setTitle(R.string.login_configuration_detection) .setIcon(R.drawable.ic_error) .setMessage(message) + .setCancelable(false) .setNeutralButton(R.string.login_view_logs) { _, _ -> val intent = DebugInfoActivity.IntentBuilder(requireActivity()) .withLogs(model.configuration?.logs) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 03510b054..c998ce993 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -288,8 +288,8 @@ No certificate found Install certificate - Configuration detection - Please wait, querying server… + Add account + Please wait, adding account… Couldn\'t find CalDAV or CardDAV service. Username (email address) / password wrong? Show details @@ -497,4 +497,5 @@ Account Manager: Connection security Account Manager has encountered an unknown certificate. Do you want to trust it? + Added account successfully -- GitLab From f864fadd0e891b1d8d2071f1ed04b2f0048d2da6 Mon Sep 17 00:00:00 2001 From: Sumit Pundir Date: Tue, 18 Feb 2020 00:01:01 +0530 Subject: [PATCH 039/285] fix accountType issue accountType was being always set to default. --- .../at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 63803309a..010518a73 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -101,6 +101,7 @@ class AccountDetailsFragment : Fragment() { v.createAccount.visibility = View.GONE model.createAccount( + requireActivity(), name, loginModel.credentials, config, @@ -146,6 +147,7 @@ class AccountDetailsFragment : Fragment() { val groupMethodName = resources.getStringArray(R.array.settings_contact_group_method_values)[idx] model.createAccount( + requireActivity(), name, loginModel.credentials!!, config, @@ -180,14 +182,13 @@ class AccountDetailsFragment : Fragment() { nameError.value = null } - fun createAccount(name: String, credentials: Credentials?, config: DavResourceFinder.Configuration, groupMethod: GroupMethod): LiveData { + fun createAccount(activity: Activity, name: String, credentials: Credentials?, config: DavResourceFinder.Configuration, groupMethod: GroupMethod): LiveData { val result = MutableLiveData() viewModelScope.launch(Dispatchers.Default + NonCancellable) { var accountType = context.getString(R.string.account_type) var addressBookAccountType = context.getString(R.string.account_type_address_book) - val intent = Intent(context, LoginActivity::class.java) - when (intent.getStringExtra("SETUP_ACCOUNT_PROVIDER_TYPE")) { + when (activity.intent.getStringExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE)) { LoginActivity.ACCOUNT_PROVIDER_EELO -> { accountType = context.getString(R.string.eelo_account_type) addressBookAccountType = context.getString(R.string.account_type_eelo_address_book) -- GitLab From f24f9570f5b92f561f997ad02bf0a38316c20298 Mon Sep 17 00:00:00 2001 From: Nihar Thakkar Date: Sat, 14 Jul 2018 11:53:43 +0530 Subject: [PATCH 040/285] Use Android AccountManager to manage OAuth tokens, create mock /e/ account for testing --- .../java/at/bitfire/davdroid/Constants.kt | 1 + .../EeloAccountAuthenticatorService.kt | 41 +- .../GoogleAccountAuthenticatorService.kt | 41 +- .../ui/setup/AccountDetailsFragment.kt | 5 + .../ui/setup/EeloAuthenticatorFragment.kt | 424 ++++-------------- .../ui/setup/GoogleAuthenticatorFragment.kt | 226 ++++++---- .../layout/fragment_eelo_authenticator.xml | 171 ++++++- 7 files changed, 438 insertions(+), 471 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/Constants.kt b/app/src/main/java/at/bitfire/davdroid/Constants.kt index a53e0486b..7e345651e 100644 --- a/app/src/main/java/at/bitfire/davdroid/Constants.kt +++ b/app/src/main/java/at/bitfire/davdroid/Constants.kt @@ -28,4 +28,5 @@ object Constants { */ const val EXCEPTION_CONTEXT_REMOTE_RESOURCE = "remoteResource" + const val AUTH_TOKEN_TYPE = "oauth2-access-token" } diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt index 0ed098d3f..0dbfa7c54 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt @@ -25,8 +25,11 @@ import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.ui.setup.LoginActivity import dagger.hilt.android.AndroidEntryPoint +import net.openid.appauth.AuthState +import net.openid.appauth.AuthorizationService import java.util.logging.Level import javax.inject.Inject import kotlin.concurrent.thread @@ -149,19 +152,43 @@ class EeloAccountAuthenticatorService : Service(), OnAccountsUpdateListener { p3: Bundle? ) = null - override fun getAuthToken( - p0: AccountAuthenticatorResponse?, - p1: Account?, - p2: String?, - p3: Bundle? - ) = null + override fun getAuthToken(response: AccountAuthenticatorResponse?, account: Account?, authTokenType: String?, options: Bundle?): Bundle { + val accountManager = AccountManager.get(context) + val authState = AuthState.jsonDeserialize(accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE)) + + if (authState != null) { + if (authState.needsTokenRefresh) { + val tokenRequest = authState.createTokenRefreshRequest() + + AuthorizationService(context).performTokenRequest(tokenRequest) { tokenResponse, ex -> + authState.update(tokenResponse, ex) + val result = Bundle() + result.putString(AccountManager.KEY_ACCOUNT_NAME, account!!.name) + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) + result.putString(AccountManager.KEY_AUTHTOKEN, authState.accessToken) + response?.onResult(result) + } + } + else { + val result = Bundle() + result.putString(AccountManager.KEY_ACCOUNT_NAME, account!!.name) + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) + result.putString(AccountManager.KEY_AUTHTOKEN, authState.accessToken) + return result + } + } + + val result = Bundle() + result.putInt(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION) + return result + } + override fun hasFeatures( p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array? ) = null - } } diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt index eaf9af5e1..2f88ed07f 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt @@ -25,8 +25,11 @@ import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.ui.setup.LoginActivity import dagger.hilt.android.AndroidEntryPoint +import net.openid.appauth.AuthState +import net.openid.appauth.AuthorizationService import java.util.logging.Level import javax.inject.Inject import kotlin.concurrent.thread @@ -147,12 +150,38 @@ class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener { p3: Bundle? ) = null - override fun getAuthToken( - p0: AccountAuthenticatorResponse?, - p1: Account?, - p2: String?, - p3: Bundle? - ) = null + + override fun getAuthToken(response: AccountAuthenticatorResponse?, account: Account?, authTokenType: String?, options: Bundle?): Bundle { + val accountManager = AccountManager.get(context) + val authState = AuthState.jsonDeserialize(accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE)) + + if (authState != null) { + if (authState.needsTokenRefresh) { + val tokenRequest = authState.createTokenRefreshRequest() + + AuthorizationService(context).performTokenRequest(tokenRequest) { tokenResponse, ex -> + authState.update(tokenResponse, ex) + val result = Bundle() + result.putString(AccountManager.KEY_ACCOUNT_NAME, account!!.name) + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) + result.putString(AccountManager.KEY_AUTHTOKEN, authState.accessToken) + response?.onResult(result) + } + } + else { + val result = Bundle() + result.putString(AccountManager.KEY_ACCOUNT_NAME, account!!.name) + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) + result.putString(AccountManager.KEY_AUTHTOKEN, authState.accessToken) + return result + } + } + + val result = Bundle() + result.putInt(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION) + return result + } + override fun hasFeatures( p0: AccountAuthenticatorResponse?, diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 010518a73..84a0f276b 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -210,6 +210,11 @@ class AccountDetailsFragment : Fragment() { return@launch } + if (!credentials?.authState?.accessToken.isNullOrEmpty()) { + val accountManager = AccountManager.get(context) + accountManager.setAuthToken(account, Constants.AUTH_TOKEN_TYPE, credentials?.authState?.accessToken) + } + // add entries for account to service DB Logger.log.log(Level.INFO, "Writing account configuration to database", config) try { diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/EeloAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/EeloAuthenticatorFragment.kt index c2bff6509..0ddc8a013 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/EeloAuthenticatorFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/EeloAuthenticatorFragment.kt @@ -16,408 +16,146 @@ package at.bitfire.davdroid.ui.setup -import android.app.Activity -import android.app.PendingIntent import android.content.Context -import android.content.Intent import android.net.ConnectivityManager -import android.os.AsyncTask import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProviders +import at.bitfire.davdroid.Constants import at.bitfire.davdroid.R -import at.bitfire.davdroid.authorization.IdentityProvider import at.bitfire.davdroid.databinding.FragmentEeloAuthenticatorBinding import at.bitfire.davdroid.db.Credentials -import net.openid.appauth.* -import org.json.JSONException -import org.json.JSONObject -import java.io.BufferedReader -import java.io.IOException -import java.io.InputStream -import java.io.InputStreamReader -import java.net.HttpURLConnection -import java.net.MalformedURLException +import kotlinx.android.synthetic.main.fragment_eelo_authenticator.* +import kotlinx.android.synthetic.main.fragment_eelo_authenticator.view.* import java.net.URI -import java.net.URL -class EeloAuthenticatorFragment : Fragment(), AuthorizationService.TokenResponseCallback { +class EeloAuthenticatorFragment : Fragment() { private lateinit var model: EeloAuthenticatorModel private lateinit var loginModel: LoginModel - private val extraAuthServiceDiscovery = "authServiceDiscovery" - private val extraClientSecret = "clientSecret" - - private var authState: AuthState? = null - private var authorizationService: AuthorizationService? = null - - private val bufferSize = 1024 - private var userInfoJson: JSONObject? = null + val TOGGLE_BUTTON_CHECKED_KEY = "toggle_button_checked" + var toggleButtonState = false private fun isNetworkAvailable(): Boolean { - val connectivityManager = - activity!!.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val connectivityManager = requireActivity().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val activeNetworkInfo = connectivityManager.activeNetworkInfo return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting } - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { model = ViewModelProviders.of(this).get(EeloAuthenticatorModel::class.java) loginModel = ViewModelProviders.of(requireActivity()).get(LoginModel::class.java) if (!isNetworkAvailable()) { - Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG) - .show() - activity!!.finish() + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + requireActivity().finish() } - // Initialise the authorization service - authorizationService = AuthorizationService(context!!) - val v = FragmentEeloAuthenticatorBinding.inflate(inflater, container, false) v.lifecycleOwner = this v.model = model - activity?.intent?.let { - model.initialize(it) - - if (!with(it) { - getBooleanExtra( - LoginActivity.ACCOUNT_PROVIDER_EELO_AUTH_COMPLETE, - false - ) - }) { - // Get all the account providers - val providers = IdentityProvider.getEnabledProviders(context) - - // Iterate over the account providers - for (idp in providers) { - val retrieveCallback = - AuthorizationServiceConfiguration.RetrieveConfigurationCallback { serviceConfiguration, ex -> - if (ex == null && serviceConfiguration != null) { - makeAuthRequest(serviceConfiguration, idp) - } else { - Toast.makeText( - context, - "Login failed, please try again later", - Toast.LENGTH_LONG - ).show() - activity!!.finish() - } - } - - if (idp.name == getString(R.string.google_name)) { - // Get configurations for the Google account provider - idp.retrieveConfig(context, retrieveCallback) - } - } - } else { - if (authState == null) { - val response = AuthorizationResponse.fromIntent(activity!!.intent) - val ex = AuthorizationException.fromIntent(activity!!.intent) - authState = AuthState(response, ex) - - if (response != null) { - exchangeAuthorizationCode(response) - } else { - Toast.makeText( - context, - "Login failed, please try again later", - Toast.LENGTH_LONG - ).show() - activity!!.finish() - } - } - } - } - - return v.root - } + v.root.server_toggle_button.setOnClickListener() { expandCollapse() } - private fun makeAuthRequest(serviceConfig: AuthorizationServiceConfiguration, idp: IdentityProvider) { + v.root.sign_in.setOnClickListener { login() } - if (!isNetworkAvailable()) { - Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG) - .show() - activity!!.finish() + v.root.urlpwd_user_name.doOnTextChanged { text, _, _, _ -> + val domain = computeDomain(text) + if (domain.isEmpty()) { + requireView().urlpwd_server_uri_layout.hint = getString(R.string.login_server_uri) + } else { + requireView().urlpwd_server_uri_layout.hint = getString(R.string.login_server_uri_custom, domain) + } } - val authRequest = AuthorizationRequest.Builder( - serviceConfig, - idp.clientId, - ResponseTypeValues.CODE, - idp.redirectUri - ) - .setScope(idp.scope) - .build() - - authorizationService?.performAuthorizationRequest( - authRequest, - createPostAuthorizationIntent( - context!!, - authRequest, - serviceConfig.discoveryDoc, - idp.clientSecret - ), - authorizationService?.createCustomTabsIntentBuilder()!! - .build() - ) - - requireActivity().setResult(Activity.RESULT_OK) - requireActivity().finish() - } - - private fun createPostAuthorizationIntent( - context: Context, - request: AuthorizationRequest, - discoveryDoc: AuthorizationServiceDiscovery?, - clientSecret: String? - ): PendingIntent { - val intent = Intent(context, LoginActivity::class.java) - - if (discoveryDoc != null) { - intent.putExtra(extraAuthServiceDiscovery, discoveryDoc.docJson.toString()) + // code below is to draw toggle button in its correct state and show or hide server url input field + //add by Vincent, 18/02/2019 + if (savedInstanceState != null) { + toggleButtonState = savedInstanceState.getBoolean(TOGGLE_BUTTON_CHECKED_KEY, false) } - if (clientSecret != null) { - intent.putExtra(extraClientSecret, clientSecret) + //This allow the button to be redraw in the correct state if user turn screen + if (toggleButtonState) { + v.root.server_toggle_button.setCompoundDrawablesWithIntrinsicBounds(null, null , resources.getDrawable(R.drawable.ic_expand_less), null) + v.root.urlpwd_server_uri_layout.setVisibility(View.VISIBLE) + v.root.urlpwd_server_uri.setEnabled(true) + } else { + v.root.server_toggle_button.setCompoundDrawablesWithIntrinsicBounds(null, null , resources.getDrawable(R.drawable.ic_expand_more), null) + v.root.urlpwd_server_uri_layout.setVisibility(View.GONE) + v.root.urlpwd_server_uri.setEnabled(false) } - - intent.putExtra( - LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, - LoginActivity.ACCOUNT_PROVIDER_EELO - ) - intent.putExtra(LoginActivity.ACCOUNT_PROVIDER_EELO_AUTH_COMPLETE, true) - - return PendingIntent.getActivity(context, request.hashCode(), intent, 0) + return v.root } - private fun exchangeAuthorizationCode(authorizationResponse: AuthorizationResponse) { - if (!isNetworkAvailable()) { - Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG) - .show() - activity!!.finish() - } - - val additionalParams = HashMap() - - if (getClientSecretFromIntent(activity!!.intent) != null) { - additionalParams["client_secret"] = getClientSecretFromIntent(activity!!.intent) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (model.blockProceedWithLogin()) { + ECloudAccountHelper.showMultipleECloudAccountNotAcceptedDialog(requireActivity()) } - - performTokenRequest(authorizationResponse.createTokenExchangeRequest(additionalParams)) } - private fun getClientSecretFromIntent(intent: Intent): String? { - return if (!intent.hasExtra(extraClientSecret)) { - null - } else intent.getStringExtra(extraClientSecret) + override fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(TOGGLE_BUTTON_CHECKED_KEY, toggleButtonState) + super.onSaveInstanceState(outState) } - - private fun performTokenRequest(request: TokenRequest) { - if (!isNetworkAvailable()) { - Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG) - .show() - activity!!.finish() + private fun computeDomain(username: CharSequence?) : String { + var domain = "" + if (!username.isNullOrEmpty() && username.toString().contains("@")) { + var dns = username.toString().substringAfter("@") + if (dns == Constants.E_SYNC_URL) { + dns = Constants.EELO_SYNC_HOST + } + domain = "https://$dns" } - - authorizationService?.performTokenRequest(request, this) - } - - override fun onTokenRequestCompleted(response: TokenResponse?, ex: AuthorizationException?) { - authState?.update(response, ex) - getAccountInfo() + return domain } - private fun getAccountInfo() { + private fun login() { if (!isNetworkAvailable()) { - Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG) - .show() - activity!!.finish() + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + requireActivity().finish() } - val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent) - - if (!authState!!.isAuthorized || discoveryDoc == null || discoveryDoc.userinfoEndpoint == null) { - Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG) - .show() - activity!!.finish() + if ((urlpwd_user_name.text.toString() != "") && (urlpwd_password.text.toString() != "")) { + if (validate()) + requireFragmentManager().beginTransaction() + .replace(android.R.id.content, DetectConfigurationFragment(), null) + .addToBackStack(null) + .commit() } else { - object : AsyncTask() { - override fun doInBackground(vararg params: Void): Void? { - - if (fetchUserInfo()) { - Toast.makeText( - context, - "Login failed, please try again later", - Toast.LENGTH_LONG - ).show() - activity!!.finish() - } - - return null - } - }.execute() - } - } - - private fun getDiscoveryDocFromIntent(intent: Intent): AuthorizationServiceDiscovery? { - if (!intent.hasExtra(extraAuthServiceDiscovery)) { - return null + Toast.makeText(context, "Please enter a valid username and password", Toast.LENGTH_LONG).show() } - val discoveryJson = intent.getStringExtra(extraAuthServiceDiscovery) - - try { - return AuthorizationServiceDiscovery(JSONObject(discoveryJson)) - } catch (ex: JSONException) { - throw IllegalStateException("Malformed JSON in discovery doc") - } catch (ex: AuthorizationServiceDiscovery.MissingArgumentException) { - throw IllegalStateException("Malformed JSON in discovery doc") - } } - private fun fetchUserInfo(): Boolean { - var error = false - - if (authState!!.authorizationServiceConfiguration == null) { - return true - } - - authState!!.performActionWithFreshTokens( - authorizationService!!, - AuthState.AuthStateAction { accessToken, _, ex -> - - if (ex != null) { - error = true - return@AuthStateAction - } - - val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent) - ?: throw IllegalStateException("no available discovery doc") - val userInfoEndpoint: URL - - try { - userInfoEndpoint = URL(discoveryDoc.userinfoEndpoint!!.toString()) - } catch (urlEx: MalformedURLException) { - error = true - return@AuthStateAction - } - - var userInfoResponse: InputStream? = null - - try { - val conn = userInfoEndpoint.openConnection() as HttpURLConnection - conn.setRequestProperty("Authorization", "Bearer " + accessToken!!) - conn.instanceFollowRedirects = false - userInfoResponse = conn.inputStream - val response = readStream(userInfoResponse) - updateUserInfo(JSONObject(response)) - } catch (ioEx: IOException) { - error = true - } catch (jsonEx: JSONException) { - error = true - } finally { - - if (userInfoResponse != null) { - try { - userInfoResponse.close() - } catch (ioEx: IOException) { - error = true - } - } - - } - }) - - return error - } - - @Throws(IOException::class) - private fun readStream(stream: InputStream?): String { - val br = BufferedReader(InputStreamReader(stream!!)) - val buffer = CharArray(bufferSize) - val sb = StringBuilder() - var readCount = br.read(buffer) - - while (readCount != -1) { - sb.append(buffer, 0, readCount) - readCount = br.read(buffer) - } - - return sb.toString() - } - - private fun updateUserInfo(jsonObject: JSONObject) { - Handler(Looper.getMainLooper()).post { - userInfoJson = jsonObject - onAccountInfoGotten() - } - } - - private fun onAccountInfoGotten() { - if (!isNetworkAvailable()) { - Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG) - .show() - activity!!.finish() - } - - if (userInfoJson != null) { - try { - var emailAddress = "" - - if (userInfoJson!!.has("email")) { - emailAddress = userInfoJson!!.getString("email") - } - - if (validate(emailAddress, authState!!)) { - requireFragmentManager().beginTransaction() - .replace(android.R.id.content, DetectConfigurationFragment(), null) - .addToBackStack(null) - .commit() - } + private fun validate(): Boolean { + var valid = false - } catch (ex: JSONException) { - Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG) - .show() - activity!!.finish() - } + var serverUrl = requireView().urlpwd_server_uri.text.toString() - } else { - Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG) - .show() - activity!!.finish() + if (serverUrl.isEmpty()) { + serverUrl = computeDomain(requireView().urlpwd_user_name.text.toString()) } - } - - private fun validate(emailAddress: String, authState: AuthState): Boolean { - var valid = false - fun validateUrl() { + model.baseUrlError.value = null try { - val uri = URI("https://apidata.googleusercontent.com/caldav/v2/$emailAddress/events") - + val uri = URI(serverUrl) if (uri.scheme.equals("http", true) || uri.scheme.equals("https", true)) { valid = true loginModel.baseURI = uri - } else { + } else model.baseUrlError.value = getString(R.string.login_url_must_be_http_or_https) - } - } catch (e: Exception) { model.baseUrlError.value = e.localizedMessage } @@ -427,11 +165,13 @@ class EeloAuthenticatorFragment : Fragment(), AuthorizationService.TokenResponse model.loginWithUrlAndTokens.value == true -> { validateUrl() - model.usernameError.value = null + + val userName = requireView().urlpwd_user_name.text.toString() + val password = requireView().urlpwd_password.text.toString() if (loginModel.baseURI != null) { valid = true - loginModel.credentials = Credentials(emailAddress, null, authState, null) + loginModel.credentials = Credentials(userName.toLowerCase(), password, null, null, loginModel.baseURI) } } @@ -440,8 +180,20 @@ class EeloAuthenticatorFragment : Fragment(), AuthorizationService.TokenResponse return valid } - override fun onDestroy() { - super.onDestroy() - authorizationService?.dispose() + /** + * Show/Hide panel containing server's uri input field. + */ + private fun expandCollapse() { + if (!toggleButtonState) { + server_toggle_button.setCompoundDrawablesWithIntrinsicBounds(null, null , resources.getDrawable(R.drawable.ic_expand_less), null) + urlpwd_server_uri_layout.setVisibility(View.VISIBLE) + urlpwd_server_uri.setEnabled(true) + toggleButtonState = true + } else { + server_toggle_button.setCompoundDrawablesWithIntrinsicBounds(null, null , resources.getDrawable(R.drawable.ic_expand_more), null) + urlpwd_server_uri_layout.setVisibility(View.GONE) + urlpwd_server_uri.setEnabled(false) + toggleButtonState = false + } } } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt index f1d937f60..7578011ec 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt @@ -20,33 +20,33 @@ import android.app.Activity import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.net.ConnectivityManager -import android.os.AsyncTask -import android.os.Bundle -import android.os.Handler -import android.os.Looper +import android.os.* +import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast -import androidx.fragment.app.Fragment + import androidx.lifecycle.ViewModelProviders -import at.bitfire.davdroid.R -import at.bitfire.davdroid.authorization.IdentityProvider -import at.bitfire.davdroid.databinding.FragmentGoogleAuthenticatorBinding -import at.bitfire.davdroid.db.Credentials -import kotlinx.android.synthetic.main.fragment_eelo_authenticator.* import net.openid.appauth.* import org.json.JSONException -import org.json.JSONObject import java.io.BufferedReader import java.io.IOException import java.io.InputStream import java.io.InputStreamReader -import java.net.HttpURLConnection -import java.net.MalformedURLException -import java.net.URI -import java.net.URL +import java.net.* +import org.json.JSONObject +import java.util.HashMap + +import kotlinx.android.synthetic.main.fragment_google_authenticator.* +import android.net.ConnectivityManager +import android.text.Layout +import android.text.SpannableString +import android.text.style.AlignmentSpan +import android.widget.Toast +import at.bitfire.davdroid.authorization.IdentityProvider +import at.bitfire.davdroid.databinding.FragmentGoogleAuthenticatorBinding +import at.bitfire.davdroid.db.Credentials +import com.google.android.material.dialog.MaterialAlertDialogBuilder class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenResponseCallback { @@ -69,14 +69,10 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View { - model = ViewModelProviders.of(this)[GoogleAuthenticatorModel::class.java] - loginModel = ViewModelProviders.of(requireActivity())[LoginModel::class.java] + savedInstanceState: Bundle?): View? { - if (!isNetworkAvailable()) { - Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() - activity!!.finish() - } + model = ViewModelProviders.of(this).get(GoogleAuthenticatorModel::class.java) + loginModel = ViewModelProviders.of(requireActivity()).get(LoginModel::class.java) // Initialise the authorization service authorizationService = AuthorizationService(context!!) @@ -87,40 +83,63 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon activity?.intent?.let { model.initialize(it) + val builder = MaterialAlertDialogBuilder(context!!, R.style.CustomAlertDialogStyle) if (!with(it) { getBooleanExtra(LoginActivity.ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE, false) }) { - // Get all the account providers - val providers = IdentityProvider.getEnabledProviders(context) - - // Iterate over the account providers - for (idp in providers) { - val retrieveCallback = AuthorizationServiceConfiguration.RetrieveConfigurationCallback { serviceConfiguration, ex -> - if (ex == null && serviceConfiguration != null) { - makeAuthRequest(serviceConfiguration, idp) - } - else { - Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() - activity!!.finish() + val title = SpannableString(getString(R.string.google_alert_title)) + // alert dialog title align center + title.setSpan( + AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), + 0, + title.length, + 0 + ) + + builder.setTitle(title) + builder.setMessage(getString(R.string.google_alert_message)) + builder.setPositiveButton(android.R.string.yes) { dialog, which -> + // Get all the account providers + val providers = IdentityProvider.getEnabledProviders(context) + + // Iterate over the account providers + for (idp in providers) { + val retrieveCallback = AuthorizationServiceConfiguration.RetrieveConfigurationCallback { serviceConfiguration, ex -> + if (ex == null && serviceConfiguration != null) { + makeAuthRequest(serviceConfiguration, idp) + } else if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } else { + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + activity!!.finish() + } } - } - if (idp.name == getString(R.string.google_name)) { - // Get configurations for the Google account provider - idp.retrieveConfig(context, retrieveCallback) + if (idp.name == getString(R.string.google_name)) { + // Get configurations for the Google account provider + idp.retrieveConfig(context, retrieveCallback) + } } } + builder.setCancelable(false) + + val dialog = builder.create() + dialog.show() + } else { if (authState == null) { - val response = AuthorizationResponse.fromIntent(activity!!.intent) + val response = AuthorizationResponse.fromIntent(activity!!.intent) val ex = AuthorizationException.fromIntent(activity!!.intent) authState = AuthState(response, ex) if (response != null) { - exchangeAuthorizationCode(response) - } - else { - Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + exchangeAuthorizationCode(response) + } else if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } else { + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() activity!!.finish() } } @@ -131,41 +150,41 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon } private fun makeAuthRequest( - serviceConfig: AuthorizationServiceConfiguration, - idp: IdentityProvider) { + serviceConfig: AuthorizationServiceConfiguration, + idp: IdentityProvider) { - if (!isNetworkAvailable()) { + if (!isNetworkAvailable()) { Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() activity!!.finish() } val authRequest = AuthorizationRequest.Builder( - serviceConfig, - idp.clientId, - ResponseTypeValues.CODE, - idp.redirectUri) - .setScope(idp.scope) - .build() + serviceConfig, + idp.clientId, + ResponseTypeValues.CODE, + idp.redirectUri) + .setScope(idp.scope) + .build() authorizationService?.performAuthorizationRequest( + authRequest, + createPostAuthorizationIntent( + context!!, authRequest, - createPostAuthorizationIntent( - context!!, - authRequest, - serviceConfig.discoveryDoc, - idp.clientSecret), - authorizationService?.createCustomTabsIntentBuilder()!! - .build()) - - requireActivity().setResult(Activity.RESULT_OK) + serviceConfig.discoveryDoc, + idp.clientSecret), + authorizationService?.createCustomTabsIntentBuilder()!! + .build()) + + requireActivity().setResult(Activity.RESULT_OK) requireActivity().finish() } private fun createPostAuthorizationIntent( - context: Context, - request: AuthorizationRequest, - discoveryDoc: AuthorizationServiceDiscovery?, - clientSecret: String?): PendingIntent { + context: Context, + request: AuthorizationRequest, + discoveryDoc: AuthorizationServiceDiscovery?, + clientSecret: String?): PendingIntent { val intent = Intent(context, LoginActivity::class.java) if (discoveryDoc != null) { @@ -183,7 +202,7 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon } private fun exchangeAuthorizationCode(authorizationResponse: AuthorizationResponse) { - if (!isNetworkAvailable()) { + if (!isNetworkAvailable()) { Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() activity!!.finish() } @@ -198,29 +217,29 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon private fun getClientSecretFromIntent(intent: Intent): String? { return if (!intent.hasExtra(extraClientSecret)) { null - } else intent.getStringExtra(extraClientSecret) + } + else intent.getStringExtra(extraClientSecret) } private fun performTokenRequest(request: TokenRequest) { - if (!isNetworkAvailable()) { + if (!isNetworkAvailable()) { Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() activity!!.finish() } authorizationService?.performTokenRequest( - request, this) + request, this) } override fun onTokenRequestCompleted(response: TokenResponse?, ex: AuthorizationException?) { authState?.update(response, ex) - progress_bar.visibility = View.GONE - successful_oauth_text_view.visibility = View.VISIBLE + getAccountInfo() } private fun getAccountInfo() { - if (!isNetworkAvailable()) { + if (!isNetworkAvailable()) { Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() activity!!.finish() } @@ -228,15 +247,15 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent) if (!authState!!.isAuthorized - || discoveryDoc == null - || discoveryDoc.userinfoEndpoint == null) { + || discoveryDoc == null + || discoveryDoc.userinfoEndpoint == null) { Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() activity!!.finish() } else { object : AsyncTask() { override fun doInBackground(vararg params: Void): Void? { - if (fetchUserInfo()) { + if (fetchUserInfo()) { Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() activity!!.finish() } @@ -273,19 +292,19 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon authState!!.performActionWithFreshTokens(authorizationService!!, AuthState.AuthStateAction { accessToken, _, ex -> if (ex != null) { - error = true + error = true return@AuthStateAction } val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent) - ?: throw IllegalStateException("no available discovery doc") + ?: throw IllegalStateException("no available discovery doc") val userInfoEndpoint: URL try { userInfoEndpoint = URL(discoveryDoc.userinfoEndpoint!!.toString()) } catch (urlEx: MalformedURLException) { - error = true + error = true return@AuthStateAction } @@ -299,10 +318,10 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon updateUserInfo(JSONObject(response)) } catch (ioEx: IOException) { - error = true + error = true } catch (jsonEx: JSONException) { - error = true + error = true } finally { if (userInfoResponse != null) { @@ -310,14 +329,14 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon userInfoResponse.close() } catch (ioEx: IOException) { - error = true + error = true } } } }) - return error + return error } @Throws(IOException::class) @@ -341,33 +360,34 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon } private fun onAccountInfoGotten() { - if (!isNetworkAvailable()) { + if (!isNetworkAvailable()) { Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() activity!!.finish() } if (userInfoJson != null) { try { + var emailAddress = "" if (userInfoJson!!.has("email")) { emailAddress = userInfoJson!!.getString("email") } - if (validate(emailAddress, authState!!)) - requireFragmentManager().beginTransaction() + if (validate(emailAddress, authState!!)) + requireFragmentManager().beginTransaction() .replace(android.R.id.content, DetectConfigurationFragment(), null) .addToBackStack(null) .commit() } catch (ex: JSONException) { - Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() activity!!.finish() } } else { - Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() activity!!.finish() } @@ -379,7 +399,7 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon fun validateUrl() { model.baseUrlError.value = null try { - val uri = URI("https://apidata.googleusercontent.com/caldav/v2/$emailAddress/events") + val uri = URI("https://apidata.googleusercontent.com/caldav/v2/$emailAddress/user") if (uri.scheme.equals("http", true) || uri.scheme.equals("https", true)) { valid = true loginModel.baseURI = uri @@ -390,14 +410,20 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon } } - if(model.loginWithUrlAndTokens.value == true) { - validateUrl() - model.usernameError.value = null - if (loginModel.baseURI != null) { - valid = true - loginModel.credentials = Credentials(emailAddress, null, authState, null) - } - } + when { + + model.loginWithUrlAndTokens.value == true -> { + validateUrl() + + model.usernameError.value = null + + if (loginModel.baseURI != null) { + valid = true + loginModel.credentials = Credentials(emailAddress, null, authState, null) + } + } + + } return valid } @@ -406,4 +432,8 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon super.onDestroy() authorizationService?.dispose() } + + } + + diff --git a/app/src/main/res/layout/fragment_eelo_authenticator.xml b/app/src/main/res/layout/fragment_eelo_authenticator.xml index 462c23a5b..76bde5014 100644 --- a/app/src/main/res/layout/fragment_eelo_authenticator.xml +++ b/app/src/main/res/layout/fragment_eelo_authenticator.xml @@ -1,37 +1,160 @@ + + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> - + + + + type="at.bitfire.davdroid.ui.setup.EeloAuthenticatorModel" /> - - - - - + + + + + + + + - - + android:layout_height="0dp" + android:layout_weight="1"> + + + + + + + + + + + + + + + + +