From 6d30ef42e437830dd3691b7766c2a66b289d2fc9 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Mon, 9 Oct 2023 15:54:21 +0200 Subject: [PATCH 01/23] Foreground service: startForeground() within 5 seconds to avoid exception (bitfireAT/davx5#405) * Call startForeground in onCreate * Add stopSelf() and comments --------- Co-authored-by: Ricki Hirner --- .../at/bitfire/davdroid/ForegroundService.kt | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ForegroundService.kt b/app/src/main/kotlin/at/bitfire/davdroid/ForegroundService.kt index d9c78b562..c5762216e 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ForegroundService.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ForegroundService.kt @@ -24,6 +24,25 @@ import dagger.hilt.components.SingletonComponent class ForegroundService : Service() { + override fun onCreate() { + super.onCreate() + + /* Call startForeground as soon as possible (must be within 5 seconds after the service has been created). + If the foreground service shouldn't remain active (because the setting has been disabled), + we'll immediately stop it with stopForeground() in onStartCommand(). */ + val settingsIntent = Intent(this, AppSettingsActivity::class.java).apply { + putExtra(AppSettingsActivity.EXTRA_SCROLL_TO, Settings.FOREGROUND_SERVICE) + } + val builder = NotificationCompat.Builder(this, NotificationUtils.CHANNEL_STATUS) + .setSmallIcon(R.drawable.ic_foreground_notify) + .setContentTitle(getString(R.string.foreground_service_notify_title)) + .setContentText(getString(R.string.foreground_service_notify_text)) + .setStyle(NotificationCompat.BigTextStyle()) + .setContentIntent(PendingIntent.getActivity(this, 0, settingsIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)) + .setCategory(NotificationCompat.CATEGORY_STATUS) + startForeground(NotificationUtils.NOTIFY_FOREGROUND, builder.build()) + } + companion object { @EntryPoint @@ -67,6 +86,7 @@ class ForegroundService : Service() { if (batteryOptimizationWhitelisted(context)) { val serviceIntent = Intent(ACTION_FOREGROUND, null, context, ForegroundService::class.java) if (Build.VERSION.SDK_INT >= 26) + // we now have 5 seconds to call Service.startForeground() [https://developer.android.com/about/versions/oreo/android-8.0-changes.html#back-all] context.startForegroundService(serviceIntent) else context.startService(serviceIntent) @@ -97,21 +117,14 @@ class ForegroundService : Service() { override fun onBind(intent: Intent?): Nothing? = null override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (foregroundServiceActivated(this)) { - val settingsIntent = Intent(this, AppSettingsActivity::class.java).apply { - putExtra(AppSettingsActivity.EXTRA_SCROLL_TO, Settings.FOREGROUND_SERVICE) - } - val builder = NotificationCompat.Builder(this, NotificationUtils.CHANNEL_STATUS) - .setSmallIcon(R.drawable.ic_foreground_notify) - .setContentTitle(getString(R.string.foreground_service_notify_title)) - .setContentText(getString(R.string.foreground_service_notify_text)) - .setStyle(NotificationCompat.BigTextStyle()) - .setContentIntent(PendingIntent.getActivity(this, 0, settingsIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)) - .setCategory(NotificationCompat.CATEGORY_STATUS) - startForeground(NotificationUtils.NOTIFY_FOREGROUND, builder.build()) + // Command is always ACTION_FOREGROUND → re-evaluate foreground setting + if (foregroundServiceActivated(this)) + // keep service open return START_STICKY - } else { + else { + // don't keep service active stopForeground(true) + stopSelf() // Stop the service so that onCreate() will run again for the next command return START_NOT_STICKY } } -- GitLab From 0ba00d7bb05176a259f187ffa30408a207db9c10 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 10 Oct 2023 09:36:43 +0100 Subject: [PATCH 02/23] Migrated `startActivityForResult` (bitfireAT/davx5#407) Migrated activity result Signed-off-by: Arnau Mora --- .../bitfire/davdroid/ui/AccountsActivity.kt | 21 +++++++------------ .../davdroid/ui/intro/IntroActivity.kt | 16 ++++++++++++++ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt index fa3c2c7d3..1722a128c 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt @@ -37,15 +37,17 @@ import javax.inject.Inject @AndroidEntryPoint class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { - companion object { - const val REQUEST_INTRO = 0 - } - @Inject lateinit var accountsDrawerHandler: AccountsDrawerHandler private lateinit var binding: ActivityAccountsBinding val model by viewModels() + private val introActivityLauncher = registerForActivityResult(IntroActivity.Contract) { cancelled -> + if (cancelled) { + finish() + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -54,8 +56,7 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele CoroutineScope(Dispatchers.Default).launch { // use a separate thread to check whether IntroActivity should be shown if (IntroActivity.shouldShowIntroActivity(this@AccountsActivity)) { - val intro = Intent(this@AccountsActivity, IntroActivity::class.java) - startActivityForResult(intro, REQUEST_INTRO) + introActivityLauncher.launch(null) } } } @@ -89,14 +90,6 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele accountsDrawerHandler.initMenu(this, binding.navView.menu) } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == REQUEST_INTRO && resultCode == Activity.RESULT_CANCELED) - finish() - else - super.onActivityResult(requestCode, resultCode, data) - } - override fun onBackPressed() { if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) binding.drawerLayout.closeDrawer(GravityCompat.START) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt index bb259812c..58dc526c3 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt @@ -5,7 +5,10 @@ package at.bitfire.davdroid.ui.intro import android.app.Activity +import android.content.Context +import android.content.Intent import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContract import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import at.bitfire.davdroid.R @@ -79,4 +82,17 @@ class IntroActivity: AppIntro2() { finish() } + + /** + * For launching the [IntroActivity]. Result is `true` when the user cancelled the intro. + */ + object Contract: ActivityResultContract() { + override fun createIntent(context: Context, input: Unit?): Intent = + Intent(context, IntroActivity::class.java) + + override fun parseResult(resultCode: Int, intent: Intent?): Boolean { + return resultCode == Activity.RESULT_CANCELED + } + } + } \ No newline at end of file -- GitLab From 3da48ab3a2807625241fe14a63f661f23f63d1f6 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 10 Oct 2023 11:25:07 +0200 Subject: [PATCH 03/23] 362 increase minimum api level to android 7 (bitfireAT/davx5#363) * Increase minSdkVersion to 24 (Android 7.0) * Remove obsolete api level checks * Use latest dnsjava * Use latest apache commons * Minor formatting * Unify getSystemService() calls * Remove further unnecessary calls * Remove noinspection GradleDependency for Apache Commons libs --------- Co-authored-by: Ricki Hirner --- app/build.gradle | 10 +- .../at/bitfire/davdroid/ForegroundService.kt | 9 +- .../davdroid/network/Android10Resolver.kt | 16 +++- .../davdroid/resource/LocalAddressBook.kt | 24 +++-- .../bitfire/davdroid/resource/LocalContact.kt | 23 +++-- .../bitfire/davdroid/resource/LocalGroup.kt | 13 ++- .../settings/AccountSettingsMigrations.kt | 9 +- .../davdroid/syncadapter/AddressBookSyncer.kt | 3 +- .../syncadapter/ContactsSyncManager.kt | 4 +- .../bitfire/davdroid/syncadapter/SyncUtils.kt | 6 +- .../davdroid/syncadapter/SyncWorker.kt | 5 +- .../davdroid/ui/AccountListFragment.kt | 11 +-- .../davdroid/ui/AppSettingsActivity.kt | 28 +++--- .../bitfire/davdroid/ui/AppWarningsManager.kt | 95 ++++++++----------- .../bitfire/davdroid/ui/DebugInfoActivity.kt | 43 ++++----- .../bitfire/davdroid/ui/NotificationUtils.kt | 13 +-- .../davdroid/ui/account/AccountActivity.kt | 33 ++----- .../ui/account/RenameAccountFragment.kt | 3 +- .../davdroid/ui/account/SettingsActivity.kt | 1 - .../davdroid/ui/account/WebcalFragment.kt | 5 +- .../ui/account/WifiPermissionsActivity.kt | 6 +- .../ui/intro/BatteryOptimizationsFragment.kt | 20 ++-- .../davdroid/ui/intro/TasksIntroFragment.kt | 7 +- .../at/bitfire/davdroid/util/CompatUtils.kt | 10 -- .../bitfire/davdroid/util/PermissionUtils.kt | 3 +- .../davdroid/webdav/RandomAccessCallback.kt | 4 +- .../bitfire/davdroid/webdav/ThumbnailCache.kt | 7 +- build.gradle | 8 +- 28 files changed, 184 insertions(+), 235 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ab3f1b215..a9dbda0fc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,7 +25,7 @@ android { setProperty "archivesBaseName", "davx5-ose-" + getVersionName() - minSdkVersion 21 // Android 5 + minSdkVersion 24 // Android 7.0 targetSdkVersion 33 // Android 13 buildConfigField "String", "userAgent", "\"DAVx5\"" @@ -182,14 +182,10 @@ dependencies { implementation "com.squareup.okhttp3:logging-interceptor:${versions.okhttp}" //noinspection GradleDependency - don't update until API level 26 (Android 8) is the minimum API [https://github.com/bitfireAT/davx5/issues/130] implementation 'commons-io:commons-io:2.8.0' - //noinspection GradleDependency - dnsjava 3+ needs Java 8/Android 7 - implementation 'dnsjava:dnsjava:2.1.9' + implementation 'dnsjava:dnsjava:3.5.2' implementation 'net.openid:appauth:0.11.1' - //noinspection GradleDependency implementation "org.apache.commons:commons-collections4:${versions.commonsCollections}" - //noinspection GradleDependency implementation "org.apache.commons:commons-lang3:${versions.commonsLang}" - //noinspection GradleDependency implementation "org.apache.commons:commons-text:${versions.commonsText}" // for tests @@ -208,4 +204,4 @@ dependencies { testImplementation "com.squareup.okhttp3:mockwebserver:${versions.okhttp}" testImplementation 'junit:junit:4.13.2' -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ForegroundService.kt b/app/src/main/kotlin/at/bitfire/davdroid/ForegroundService.kt index c5762216e..7dab5f896 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ForegroundService.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ForegroundService.kt @@ -12,6 +12,7 @@ import android.os.Build import android.os.PowerManager import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.content.getSystemService import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.ui.AppSettingsActivity @@ -62,12 +63,8 @@ class ForegroundService : Service() { * Whether the app is currently exempted from battery optimization. * @return true if battery optimization is not applied to the current app; false if battery optimization is applied */ - fun batteryOptimizationWhitelisted(context: Context) = - if (Build.VERSION.SDK_INT >= 23) { // battery optimization exists since Android 6 (SDK level 23) - val powerManager = context.getSystemService(PowerManager::class.java) - powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) - } else - true + private fun batteryOptimizationWhitelisted(context: Context) = + context.getSystemService()!!.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) /** * Whether the foreground service is enabled (checked) in the app settings. diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/Android10Resolver.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/Android10Resolver.kt index 2479b7aa9..892fe0b15 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/Android10Resolver.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/Android10Resolver.kt @@ -11,10 +11,12 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor import kotlinx.coroutines.runBlocking +import org.xbill.DNS.EDNSOption import org.xbill.DNS.Message import org.xbill.DNS.Resolver import org.xbill.DNS.ResolverListener import org.xbill.DNS.TSIG +import java.time.Duration /** * dnsjava Resolver that uses Android's [DnsResolver] API, which is available since Android 10. @@ -42,6 +44,7 @@ object Android10Resolver: Resolver { future.await() } + @Deprecated("Deprecated in dnsjava") override fun sendAsync(query: Message, listener: ResolverListener) = // currently not used by dnsjava, so no need to implement it throw NotImplementedError() @@ -63,7 +66,12 @@ object Android10Resolver: Resolver { // not applicable } - override fun setEDNS(level: Int, payloadSize: Int, flags: Int, options: MutableList?) { + override fun setEDNS( + version: Int, + payloadSize: Int, + flags: Int, + options: MutableList? + ) { // not applicable } @@ -71,12 +79,18 @@ object Android10Resolver: Resolver { // not applicable } + @Deprecated("Deprecated in dnsjava") override fun setTimeout(secs: Int, msecs: Int) { // not applicable } + @Deprecated("Deprecated in dnsjava") override fun setTimeout(secs: Int) { // not applicable } + override fun setTimeout(timeout: Duration?) { + // not applicable + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt index e2290d12a..e0c8d68d2 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt @@ -5,7 +5,11 @@ package at.bitfire.davdroid.resource import android.accounts.Account import android.accounts.AccountManager -import android.content.* +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context import android.os.Build import android.os.Bundle import android.os.RemoteException @@ -22,9 +26,13 @@ import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.util.setAndVerifyUserData -import at.bitfire.vcard4android.* +import at.bitfire.vcard4android.AndroidAddressBook +import at.bitfire.vcard4android.AndroidContact +import at.bitfire.vcard4android.AndroidGroup +import at.bitfire.vcard4android.Constants +import at.bitfire.vcard4android.GroupMethod import java.io.ByteArrayOutputStream -import java.util.* +import java.util.LinkedList import java.util.logging.Level /** @@ -266,11 +274,7 @@ open class LocalAddressBook( fun delete() { val accountManager = AccountManager.get(context) - @Suppress("DEPRECATION") - if (Build.VERSION.SDK_INT >= 22) - accountManager.removeAccount(account, null, null, null) - else - accountManager.removeAccount(account, null, null) + accountManager.removeAccount(account, null, null, null) } @@ -375,8 +379,8 @@ open class LocalAddressBook( * @throws RemoteException on content provider errors */ fun verifyDirty(): Int { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - throw IllegalStateException("verifyDirty() should not be called on Android != 7") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + throw IllegalStateException("verifyDirty() should not be called on Android != 7.0") var reallyDirty = 0 for (contact in findDirtyContacts()) { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalContact.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalContact.kt index a1c6afc20..dffc2b41a 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalContact.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalContact.kt @@ -12,12 +12,21 @@ import android.provider.ContactsContract.CommonDataKinds.GroupMembership import android.provider.ContactsContract.RawContacts.Data import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.log.Logger -import at.bitfire.davdroid.resource.contactrow.* -import at.bitfire.vcard4android.* +import at.bitfire.davdroid.resource.contactrow.CachedGroupMembershipHandler +import at.bitfire.davdroid.resource.contactrow.GroupMembershipBuilder +import at.bitfire.davdroid.resource.contactrow.GroupMembershipHandler +import at.bitfire.davdroid.resource.contactrow.UnknownPropertiesBuilder +import at.bitfire.davdroid.resource.contactrow.UnknownPropertiesHandler +import at.bitfire.vcard4android.AndroidAddressBook +import at.bitfire.vcard4android.AndroidContact +import at.bitfire.vcard4android.AndroidContactFactory +import at.bitfire.vcard4android.BatchOperation +import at.bitfire.vcard4android.CachedGroupMembership +import at.bitfire.vcard4android.Contact import ezvcard.Ezvcard import org.apache.commons.lang3.StringUtils import java.io.FileNotFoundException -import java.util.* +import java.util.UUID class LocalContact: AndroidContact, LocalAddress { @@ -91,7 +100,7 @@ class LocalContact: AndroidContact, LocalAddress { values.put(COLUMN_ETAG, eTag) values.put(ContactsContract.RawContacts.DIRTY, 0) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // workaround for Android 7 which sets DIRTY flag when only meta-data is changed val hashCode = dataHashCode() values.put(COLUMN_HASHCODE, hashCode) @@ -132,7 +141,7 @@ class LocalContact: AndroidContact, LocalAddress { * @return hash code of contact data (including group memberships) */ internal fun dataHashCode(): Int { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) throw IllegalStateException("dataHashCode() should not be called on Android != 7") // reset contact so that getContact() reads from database @@ -146,7 +155,7 @@ class LocalContact: AndroidContact, LocalAddress { } fun updateHashCode(batch: BatchOperation?) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) throw IllegalStateException("updateHashCode() should not be called on Android != 7") val hashCode = dataHashCode() @@ -163,7 +172,7 @@ class LocalContact: AndroidContact, LocalAddress { } fun getLastHashCode(): Int { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) throw IllegalStateException("getLastHashCode() should not be called on Android != 7") addressBook.provider!!.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null)?.use { c -> diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalGroup.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalGroup.kt index 56e555a97..25db4e734 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalGroup.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalGroup.kt @@ -15,9 +15,16 @@ import android.provider.ContactsContract.Groups import android.provider.ContactsContract.RawContacts import android.provider.ContactsContract.RawContacts.Data import at.bitfire.davdroid.log.Logger -import at.bitfire.vcard4android.* +import at.bitfire.vcard4android.AndroidAddressBook +import at.bitfire.vcard4android.AndroidContact +import at.bitfire.vcard4android.AndroidGroup +import at.bitfire.vcard4android.AndroidGroupFactory +import at.bitfire.vcard4android.BatchOperation +import at.bitfire.vcard4android.CachedGroupMembership +import at.bitfire.vcard4android.Contact import org.apache.commons.lang3.StringUtils -import java.util.* +import java.util.LinkedList +import java.util.UUID class LocalGroup: AndroidGroup, LocalAddress { @@ -80,7 +87,7 @@ class LocalGroup: AndroidGroup, LocalAddress { changeContactIDs += missingMember.id!! } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) // workaround for Android 7 which sets DIRTY flag when only meta-data is changed changeContactIDs .map { addressBook.findContactById(it) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt index 25ec7ed94..f8eab9dfb 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt @@ -24,7 +24,6 @@ import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.resource.LocalTask import at.bitfire.davdroid.resource.TaskUtils import at.bitfire.davdroid.syncadapter.SyncUtils -import at.bitfire.davdroid.util.closeCompat import at.bitfire.davdroid.util.setAndVerifyUserData import at.bitfire.ical4android.AndroidCalendar import at.bitfire.ical4android.AndroidEvent @@ -216,7 +215,7 @@ class AccountSettingsMigrations( } } } finally { - provider.closeCompat() + provider.close() } } } @@ -256,7 +255,7 @@ class AccountSettingsMigrations( provider.update( CalendarContract.Calendars.CONTENT_URI.asSyncAdapter(account), AndroidCalendar.calendarBaseValues, null, null) - provider.closeCompat() + provider.close() } } @@ -313,7 +312,7 @@ class AccountSettingsMigrations( try { AndroidCalendar.insertColors(provider, account) } finally { - provider.closeCompat() + provider.close() } } @@ -377,7 +376,7 @@ class AccountSettingsMigrations( throw ContactsStorageException("Couldn't migrate contacts to new address book", e) } finally { parcel.recycle() - provider.closeCompat() + provider.close() } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AddressBookSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AddressBookSyncer.kt index 5d1bbabb5..7e8e495f9 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AddressBookSyncer.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AddressBookSyncer.kt @@ -19,7 +19,6 @@ import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager -import at.bitfire.davdroid.util.closeCompat import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors @@ -115,7 +114,7 @@ class AddressBookSyncer(context: Context): Syncer(context) { LocalAddressBook.create(context, contactsProvider, account, info, forceAllReadOnly) } } finally { - contactsProvider?.closeCompat() + contactsProvider?.close() } return true diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt index 398f6e166..3172d21cd 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt @@ -109,7 +109,7 @@ class ContactsSyncManager( override fun prepare(): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // workaround for Android 7 which sets DIRTY flag when only meta-data is changed val reallyDirty = localCollection.verifyDirty() val deleted = localCollection.findDeleted().size @@ -371,7 +371,7 @@ class ContactsSyncManager( syncResult.stats.numInserts++ } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) // workaround for Android 7 which sets DIRTY flag when only meta-data is changed (local as? LocalContact)?.updateHashCode(null) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncUtils.kt index b25a98a8e..9cec0ef13 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncUtils.kt @@ -13,7 +13,6 @@ import android.content.Intent import android.content.pm.PackageManager import android.graphics.drawable.BitmapDrawable import android.net.Uri -import android.os.Build import android.provider.CalendarContract import android.provider.ContactsContract import androidx.annotation.WorkerThread @@ -78,10 +77,7 @@ object SyncUtils { } val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${e.provider.packageName}")) - - var flags = PendingIntent.FLAG_UPDATE_CURRENT - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - flags = flags or PendingIntent.FLAG_IMMUTABLE + val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE if (intent.resolveActivity(pm) != null) notify.setContentIntent(PendingIntent.getActivity(context, 0, intent, flags)) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncWorker.kt index 63498aa76..be350e150 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncWorker.kt @@ -47,7 +47,6 @@ import at.bitfire.davdroid.ui.NotificationUtils import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible import at.bitfire.davdroid.ui.account.WifiPermissionsActivity import at.bitfire.davdroid.util.PermissionUtils -import at.bitfire.davdroid.util.closeCompat import at.bitfire.ical4android.TaskProvider import com.google.common.util.concurrent.ListenableFuture import dagger.assisted.Assisted @@ -312,7 +311,7 @@ class SyncWorker @AssistedInject constructor( // Check internet connection val ignoreVpns = AccountSettings(applicationContext, account).getIgnoreVpns() val connectivityManager = applicationContext.getSystemService()!! - if (Build.VERSION.SDK_INT >= 23 && !internetAvailable(connectivityManager, ignoreVpns)) { + if (!internetAvailable(connectivityManager, ignoreVpns)) { Logger.log.info("WorkManager started SyncWorker without Internet connection. Aborting.") return Result.failure() } @@ -368,7 +367,7 @@ class SyncWorker @AssistedInject constructor( } catch (e: SecurityException) { Logger.log.log(Level.WARNING, "Security exception when opening content provider for $authority") } finally { - provider.closeCompat() + provider.close() } // Check for errors diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountListFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountListFragment.kt index 34f711343..13d6288f2 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountListFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountListFragment.kt @@ -15,7 +15,6 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.net.Uri -import android.os.Build import android.os.Bundle import android.provider.Settings import android.view.LayoutInflater @@ -108,14 +107,12 @@ class AccountListFragment: Fragment() { } model.dataSaverOn.observe(viewLifecycleOwner) { datasaverOn -> - binding.datasaverOnInfo.visibility = if (Build.VERSION.SDK_INT >= 24 && datasaverOn) View.VISIBLE else View.GONE + binding.datasaverOnInfo.visibility = if (datasaverOn) View.VISIBLE else View.GONE } binding.manageDatasaver.setOnClickListener { - if (Build.VERSION.SDK_INT >= 24) { - val intent = Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS, Uri.parse("package:" + requireActivity().packageName)) - if (intent.resolveActivity(requireActivity().packageManager) != null) - startActivity(intent) - } + val intent = Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS, Uri.parse("package:" + requireActivity().packageName)) + if (intent.resolveActivity(requireActivity().packageManager) != null) + startActivity(intent) } // Accounts adapter diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt index 9ed784ff9..b6b976017 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt @@ -15,6 +15,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.UiThread import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.getSystemService import androidx.preference.* import at.bitfire.cert4android.CustomCertStore import at.bitfire.davdroid.BuildConfig @@ -148,21 +149,18 @@ class AppSettingsActivity: AppCompatActivity() { // debug settings findPreference(Settings.BATTERY_OPTIMIZATION)!!.apply { // battery optimization exists since Android 6 (API level 23) - if (Build.VERSION.SDK_INT >= 23) { - val powerManager = requireActivity().getSystemService(PowerManager::class.java) - val whitelisted = powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) - isChecked = whitelisted - isEnabled = !whitelisted - onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, nowChecked -> - if (nowChecked as Boolean) - onBatteryOptimizationResult.launch(Intent( - android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, - Uri.parse("package:" + BuildConfig.APPLICATION_ID) - )) - false - } - } else - isVisible = false + val powerManager = requireActivity().getSystemService()!! + val whitelisted = powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) + isChecked = whitelisted + isEnabled = !whitelisted + onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, nowChecked -> + if (nowChecked as Boolean) + onBatteryOptimizationResult.launch(Intent( + android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:" + BuildConfig.APPLICATION_ID) + )) + false + } } findPreference(Settings.FOREGROUND_SERVICE)!!.apply { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppWarningsManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppWarningsManager.kt index f09a1bf86..fdbf9214e 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppWarningsManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppWarningsManager.kt @@ -4,7 +4,12 @@ package at.bitfire.davdroid.ui -import android.content.* +import android.content.BroadcastReceiver +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.SyncStatusObserver import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities @@ -62,17 +67,15 @@ class AppWarningsManager @Inject constructor( watchConnectivity() // Data saver - if (Build.VERSION.SDK_INT >= 24) { - val listener = object: BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - checkDataSaver() - } + val listener = object: BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + checkDataSaver() } - - val dataSaverChangedFilter = IntentFilter(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED) - context.registerReceiver(listener, dataSaverChangedFilter) - dataSaverChangedListener = listener } + + val dataSaverChangedFilter = IntentFilter(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED) + context.registerReceiver(listener, dataSaverChangedFilter) + dataSaverChangedListener = listener checkDataSaver() } @@ -81,61 +84,39 @@ class AppWarningsManager @Inject constructor( } private fun watchConnectivity() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // API level <26 - networkReceiver = object: BroadcastReceiver() { - init { - update() - } - - override fun onReceive(context: Context?, intent: Intent?) = update() + networkAvailable.postValue(false) + + // check for working (e.g. WiFi after captive portal login) Internet connection + val networkRequest = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + .build() + val callback = object: ConnectivityManager.NetworkCallback() { + val availableNetworks = hashSetOf() + + override fun onAvailable(network: Network) { + availableNetworks += network + update() + } - private fun update() { - networkAvailable.postValue(connectivityManager.allNetworkInfo.any { it.isConnected }) - } + override fun onLost(network: Network) { + availableNetworks -= network + update() } - @Suppress("DEPRECATION") - context.registerReceiver(networkReceiver, - IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) - ) - - } else { // API level >= 26 - networkAvailable.postValue(false) - - // check for working (e.g. WiFi after captive portal login) Internet connection - val networkRequest = NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - .build() - val callback = object: ConnectivityManager.NetworkCallback() { - val availableNetworks = hashSetOf() - - override fun onAvailable(network: Network) { - availableNetworks += network - update() - } - - override fun onLost(network: Network) { - availableNetworks -= network - update() - } - - private fun update() { - networkAvailable.postValue(availableNetworks.isNotEmpty()) - } + + private fun update() { + networkAvailable.postValue(availableNetworks.isNotEmpty()) } - connectivityManager.registerNetworkCallback(networkRequest, callback) - networkCallback = callback } + connectivityManager.registerNetworkCallback(networkRequest, callback) + networkCallback = callback } private fun checkDataSaver() { dataSaverEnabled.postValue( - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - context.getSystemService()?.let { connectivityManager -> - connectivityManager.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED - } - else - false + context.getSystemService()?.let { connectivityManager -> + connectivityManager.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED + } ) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt index 45703adea..14f5cdfe8 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt @@ -43,7 +43,6 @@ import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker import at.bitfire.davdroid.syncadapter.SyncWorker -import at.bitfire.davdroid.util.closeCompat import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.TaskProvider.ProviderName import at.techbee.jtx.JtxContract @@ -375,10 +374,7 @@ class DebugInfoActivity : AppCompatActivity() { } // system info - val locales: Any = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - LocaleList.getAdjustedDefault() - else - Locale.getDefault() + val locales: Any = LocaleList.getAdjustedDefault() writer.append( "\nSYSTEM INFORMATION\n\n" + "Android version: ${Build.VERSION.RELEASE} (${Build.DISPLAY})\n" + @@ -397,7 +393,7 @@ class DebugInfoActivity : AppCompatActivity() { // connectivity context.getSystemService()?.let { connectivityManager -> writer.append("\nCONNECTIVITY\n\n") - val activeNetwork = if (Build.VERSION.SDK_INT >= 23) connectivityManager.activeNetwork else null + val activeNetwork = connectivityManager.activeNetwork connectivityManager.allNetworks.sortedByDescending { it == activeNetwork }.forEach { network -> val properties = connectivityManager.getLinkProperties(network) connectivityManager.getNetworkCapabilities(network)?.let { capabilities -> @@ -417,19 +413,17 @@ class DebugInfoActivity : AppCompatActivity() { } writer.append('\n') - if (Build.VERSION.SDK_INT >= 23) - connectivityManager.defaultProxy?.let { proxy -> - writer.append("System default proxy: ${proxy.host}:${proxy.port}\n") + connectivityManager.defaultProxy?.let { proxy -> + writer.append("System default proxy: ${proxy.host}:${proxy.port}\n") + } + writer.append("Data saver: ").append( + when (connectivityManager.restrictBackgroundStatus) { + ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED -> "enabled" + ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED -> "whitelisted" + ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED -> "disabled" + else -> connectivityManager.restrictBackgroundStatus.toString() } - if (Build.VERSION.SDK_INT >= 24) - writer.append("Data saver: ").append( - when (connectivityManager.restrictBackgroundStatus) { - ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED -> "enabled" - ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED -> "whitelisted" - ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED -> "disabled" - else -> connectivityManager.restrictBackgroundStatus.toString() - } - ).append('\n') + ).append('\n') writer.append('\n') } @@ -443,12 +437,11 @@ class DebugInfoActivity : AppCompatActivity() { writer.append(" (RESTRICTED!)") writer.append('\n') } - if (Build.VERSION.SDK_INT >= 23) - context.getSystemService()?.let { powerManager -> - writer.append("Power saving disabled: ") - .append(if (powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)) "yes" else "no") - .append('\n') - } + context.getSystemService()?.let { powerManager -> + writer.append("Power saving disabled: ") + .append(if (powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)) "yes" else "no") + .append('\n') + } // system-wide sync writer.append("System-wide synchronization: ") .append(if (ContentResolver.getMasterSyncAutomatically()) "automatically" else "manually") @@ -648,7 +641,7 @@ class DebugInfoActivity : AppCompatActivity() { } catch (e: Exception) { nrEntries = e.toString() } finally { - client?.closeCompat() + client?.close() } val accountSettings = AccountSettings(context, account) table.addLine( diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationUtils.kt index 8f30e6636..e41544d53 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/NotificationUtils.kt @@ -15,7 +15,6 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.getSystemService import androidx.core.content.res.ResourcesCompat -import at.bitfire.davdroid.App import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import java.util.logging.Level @@ -77,15 +76,9 @@ object NotificationUtils { } } - fun newBuilder(context: Context, channel: String): NotificationCompat.Builder { - val builder = NotificationCompat.Builder(context, channel) - .setColor(ResourcesCompat.getColor(context.resources, R.color.primaryColor, null)) - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) - builder.setLargeIcon(App.getLauncherBitmap(context)) - - return builder - } + fun newBuilder(context: Context, channel: String): NotificationCompat.Builder = + NotificationCompat.Builder(context, channel) + .setColor(ResourcesCompat.getColor(context.resources, R.color.primaryColor, null)) fun NotificationManagerCompat.notifyIfPossible(tag: String?, id: Int, notification: Notification) { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt index 2e9c43834..8154948e7 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt @@ -9,7 +9,6 @@ import android.accounts.AccountManager import android.accounts.OnAccountsUpdateListener import android.app.Application import android.content.Intent -import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper @@ -141,28 +140,16 @@ class AccountActivity: AppCompatActivity() { private fun deleteAccount() { val accountManager = AccountManager.get(this) - if (Build.VERSION.SDK_INT >= 22) - accountManager.removeAccount(model.account, this, { future -> - try { - if (future.result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT)) - Handler(Looper.getMainLooper()).post { - finish() - } - } catch(e: Exception) { - Logger.log.log(Level.SEVERE, "Couldn't remove account", e) - } - }, null) - else - accountManager.removeAccount(model.account, { future -> - try { - if (future.result) - Handler(Looper.getMainLooper()).post { - finish() - } - } catch (e: Exception) { - Logger.log.log(Level.SEVERE, "Couldn't remove account", e) - } - }, null) + accountManager.removeAccount(model.account, this, { future -> + try { + if (future.result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT)) + Handler(Looper.getMainLooper()).post { + finish() + } + } catch(e: Exception) { + Logger.log.log(Level.SEVERE, "Couldn't remove account", e) + } + }, null) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/RenameAccountFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/RenameAccountFragment.kt index ee2f37214..20e135e33 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/RenameAccountFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/RenameAccountFragment.kt @@ -35,7 +35,6 @@ import at.bitfire.davdroid.resource.LocalTaskList import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.syncadapter.AccountsCleanupWorker import at.bitfire.davdroid.syncadapter.SyncWorker -import at.bitfire.davdroid.util.closeCompat import at.bitfire.ical4android.TaskProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint @@ -211,7 +210,7 @@ class RenameAccountFragment: DialogFragment() { addressBook.mainAccount = Account(newName, oldAccount.type) } } finally { - provider.closeCompat() + provider.close() } } } catch (e: Exception) { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/SettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/SettingsActivity.kt index 06b60718e..a15c0ba68 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/SettingsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/SettingsActivity.kt @@ -221,7 +221,6 @@ class SettingsActivity: AppCompatActivity() { model.ignoreVpns.observe(viewLifecycleOwner) { ignoreVpns -> it.isEnabled = true it.isChecked = ignoreVpns - it.isVisible = Build.VERSION.SDK_INT >= 23 it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, prefValue -> model.updateIgnoreVpns(prefValue as Boolean) false diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt index 107674389..56a1db3fd 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt @@ -22,13 +22,12 @@ import androidx.lifecycle.* import androidx.room.Transaction import at.bitfire.dav4jvm.UrlUtils import at.bitfire.davdroid.Constants -import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.davdroid.R -import at.bitfire.davdroid.util.closeCompat import at.bitfire.davdroid.databinding.AccountCaldavItemBinding import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.util.PermissionUtils import com.google.android.material.snackbar.Snackbar import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -204,7 +203,7 @@ class WebcalFragment: CollectionsFragment() { } fun disconnect() { - value?.closeCompat() + value?.close() value = null } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt index b9d8bd3ba..16530fcad 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt @@ -21,15 +21,15 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.app.TaskStackBuilder import androidx.core.content.ContextCompat -import androidx.core.content.ContextCompat.getSystemService +import androidx.core.content.getSystemService import androidx.core.location.LocationManagerCompat import androidx.core.text.HtmlCompat import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData -import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.davdroid.R import at.bitfire.davdroid.databinding.ActivityWifiPermissionsBinding import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.util.PermissionUtils class WifiPermissionsActivity: AppCompatActivity() { @@ -148,7 +148,7 @@ class WifiPermissionsActivity: AppCompatActivity() { // Android 9+: location service if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - getSystemService(getApplication(), LocationManager::class.java)?.let { locationManager -> + getApplication().getSystemService()?.let { locationManager -> val locationEnabled = LocationManagerCompat.isLocationEnabled(locationManager) isLocationEnabled.value = locationEnabled needLocationEnabled.value = locationEnabled diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt index c578b2c6f..c6a8df5da 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt @@ -55,14 +55,14 @@ class BatteryOptimizationsFragment: Fragment() { binding.lifecycleOwner = viewLifecycleOwner binding.model = model - model.shouldBeWhitelisted.observe(viewLifecycleOwner, { shouldBeWhitelisted -> + model.shouldBeWhitelisted.observe(viewLifecycleOwner) { shouldBeWhitelisted -> @SuppressLint("BatteryLife") - if (shouldBeWhitelisted && !model.isWhitelisted.value!! && Build.VERSION.SDK_INT >= 23) - startActivityForResult(Intent( - android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, - Uri.parse("package:" + BuildConfig.APPLICATION_ID) - ), REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) - }) + if (shouldBeWhitelisted && !model.isWhitelisted.value!!) + startActivityForResult(Intent( + android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:" + BuildConfig.APPLICATION_ID) + ), REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + } binding.batteryText.text = getString(R.string.intro_battery_text, getString(R.string.app_name)) binding.autostartHeading.text = getString(R.string.intro_autostart_title, WordUtils.capitalize(Build.MANUFACTURER)) @@ -135,11 +135,7 @@ class BatteryOptimizationsFragment: Fragment() { (evilManufacturers.contains(Build.MANUFACTURER.lowercase(Locale.ROOT)) || BuildConfig.DEBUG) fun isWhitelisted(context: Context) = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val powerManager = context.getSystemService()!! - powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) - } else - true + context.getSystemService()!!.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) } val shouldBeWhitelisted = MutableLiveData() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroFragment.kt index b92801fdf..9a2916a9d 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroFragment.kt @@ -5,7 +5,6 @@ package at.bitfire.davdroid.ui.intro import android.content.Context -import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -30,10 +29,6 @@ class TasksIntroFragment : Fragment() { ): IntroFragmentFactory { override fun getOrder(context: Context): Int { - // On Android <6, OpenTasks must be installed before DAVx5, so this fragment is not useful. - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) - return IntroFragmentFactory.DONT_SHOW - return if (!TaskUtils.isAvailable(context) && settingsManager.getBooleanOrNull(TasksFragment.Model.HINT_OPENTASKS_NOT_INSTALLED) != false) 10 else @@ -44,4 +39,4 @@ class TasksIntroFragment : Fragment() { } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/CompatUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/CompatUtils.kt index 1ff99ba94..4b9889ae8 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/CompatUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/CompatUtils.kt @@ -6,8 +6,6 @@ package at.bitfire.davdroid.util import android.accounts.Account import android.accounts.AccountManager -import android.content.ContentProviderClient -import android.os.Build import at.bitfire.davdroid.log.Logger /** @@ -26,12 +24,4 @@ fun AccountManager.setAndVerifyUserData(account: Account, key: String, value: St Thread.sleep(100) } Logger.log.warning("AccountManager failed to set $account user data $key := $value") -} - -@Suppress("DEPRECATION") -fun ContentProviderClient.closeCompat() { - if (Build.VERSION.SDK_INT >= 24) - close() - else - release() } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index 433267260..d23070485 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -15,6 +15,7 @@ import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService import androidx.core.location.LocationManagerCompat import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.R @@ -61,7 +62,7 @@ object PermissionUtils { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) true // Android <9 doesn't require active location services else - ContextCompat.getSystemService(context, LocationManager::class.java)?.let { locationManager -> + context.getSystemService()?.let { locationManager -> LocationManagerCompat.isLocationEnabled(locationManager) } ?: /* location feature not available on this device */ false diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallback.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallback.kt index a1e4099e5..0b3a26f27 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallback.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallback.kt @@ -15,7 +15,7 @@ import android.system.ErrnoException import android.system.OsConstants import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService import at.bitfire.dav4jvm.DavResource import at.bitfire.dav4jvm.HttpUtils import at.bitfire.dav4jvm.exception.DavException @@ -63,7 +63,7 @@ class RandomAccessCallback private constructor( Logger.log.info("Creating memory cache") - val maxHeapSizeMB = ContextCompat.getSystemService(context, ActivityManager::class.java)!!.memoryClass + val maxHeapSizeMB = context.getSystemService()!!.memoryClass val cacheSize = maxHeapSizeMB * FileUtils.ONE_MB.toInt() / 2 val newCache = MemorySegmentCache(cacheSize) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/ThumbnailCache.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/ThumbnailCache.kt index cb8f4c139..d456aaeeb 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/webdav/ThumbnailCache.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/ThumbnailCache.kt @@ -9,14 +9,15 @@ import android.graphics.Point import android.os.Build import android.os.storage.StorageManager import androidx.annotation.WorkerThread -import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService import at.bitfire.davdroid.db.WebDavDocument import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.webdav.cache.CacheUtils import at.bitfire.davdroid.webdav.cache.DiskCache import org.apache.commons.io.FileUtils import java.io.File -import java.util.* +import java.util.LinkedList +import java.util.UUID @WorkerThread class ThumbnailCache(context: Context) { @@ -24,7 +25,7 @@ class ThumbnailCache(context: Context) { val cache: DiskCache init { - val storageManager = ContextCompat.getSystemService(context, StorageManager::class.java)!! + val storageManager = context.getSystemService()!! val cacheDir = File(context.cacheDir, "webdav/thumbnail") val maxBytes = if (Build.VERSION.SDK_INT >= 26) storageManager.getCacheQuotaBytes(storageManager.getUuidForPath(cacheDir)) / 2 diff --git a/build.gradle b/build.gradle index 09c0ad369..b0b1646b7 100644 --- a/build.gradle +++ b/build.gradle @@ -13,10 +13,10 @@ buildscript { okhttp: '4.11.0', room: '2.5.2', workManager: '2.8.1', - // latest Apache Commons versions that don't require Java 8 (Android 7) - commonsCollections: '4.2', - commonsLang: '3.8.1', - commonsText: '1.3', + // Apache Commons versions + commonsCollections: '4.4', + commonsLang: '3.13.0', + commonsText: '1.10.0', // own libraries cert4android: '2bb3898', dav4jvm: 'da94a8b', -- GitLab From 4ce6fcbf44c1f1dedd9d84b805f1876083de4b93 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 10 Oct 2023 17:41:08 +0100 Subject: [PATCH 04/23] Replaced all `onBackPressed` usages (bitfireAT/davx5#406) * Replaced all `onBackPressed` usages Signed-off-by: Arnau Mora * Added missing finish Signed-off-by: Arnau Mora * Added more finish statements Signed-off-by: Arnau Mora --------- Signed-off-by: Arnau Mora --- .../at/bitfire/davdroid/ui/AccountsActivity.kt | 16 +++++++++------- .../bitfire/davdroid/ui/intro/IntroActivity.kt | 16 ++++++++++------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt index 1722a128c..808db922d 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt @@ -12,6 +12,7 @@ import android.content.pm.ShortcutManager import android.os.Build import android.os.Bundle import android.view.MenuItem +import androidx.activity.addCallback import androidx.activity.viewModels import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity @@ -80,6 +81,14 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele binding.navView.setNavigationItemSelectedListener(this) binding.navView.itemIconTintList = null + onBackPressedDispatcher.addCallback(this) { + if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { + binding.drawerLayout.closeDrawer(GravityCompat.START) + } else { + finish() + } + } + // handle "Sync all" intent from launcher shortcut if (savedInstanceState == null && intent.action == Intent.ACTION_SYNC) syncAllAccounts() @@ -90,13 +99,6 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele accountsDrawerHandler.initMenu(this, binding.navView.menu) } - override fun onBackPressed() { - if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) - binding.drawerLayout.closeDrawer(GravityCompat.START) - else - super.onBackPressed() - } - override fun onNavigationItemSelected(item: MenuItem): Boolean { accountsDrawerHandler.onNavigationItemSelected(this, item) binding.drawerLayout.closeDrawer(GravityCompat.START) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt index 58dc526c3..914c3ac29 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt @@ -9,6 +9,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.addCallback import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import at.bitfire.davdroid.R @@ -63,6 +64,15 @@ class IntroActivity: AppIntro2() { setBarColor(ResourcesCompat.getColor(resources, R.color.primaryDarkColor, null)) isSkipButtonEnabled = false + + onBackPressedDispatcher.addCallback(this) { + if (currentSlide == 0) { + setResult(Activity.RESULT_CANCELED) + finish() + } else { + goToPreviousSlide() + } + } } override fun onPageSelected(position: Int) { @@ -70,12 +80,6 @@ class IntroActivity: AppIntro2() { currentSlide = position } - override fun onBackPressed() { - if (currentSlide == 0) - setResult(Activity.RESULT_CANCELED) - super.onBackPressed() - } - override fun onDonePressed(currentFragment: Fragment?) { super.onDonePressed(currentFragment) setResult(Activity.RESULT_OK) -- GitLab From c8cd6d780c5065a1f1798123d63d50f56b490769 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sat, 14 Oct 2023 11:37:41 +0200 Subject: [PATCH 05/23] [WebDAV] Add timeout for RandomAccessCallback notification (bitfireAT/davx5#408) * [WIP] Add timeout for RandomAccessCallback * Use state machine to handle timeout * Use sealed class for states, guard callback access with correct states --- app/build.gradle | 1 + .../davdroid/webdav/DavDocumentsProvider.kt | 8 +- .../davdroid/webdav/RandomAccessCallback.kt | 162 +++++++++++++++--- .../webdav/StreamingFileDescriptor.kt | 5 + 4 files changed, 149 insertions(+), 27 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a9dbda0fc..31ef4acaf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -183,6 +183,7 @@ dependencies { //noinspection GradleDependency - don't update until API level 26 (Android 8) is the minimum API [https://github.com/bitfireAT/davx5/issues/130] implementation 'commons-io:commons-io:2.8.0' implementation 'dnsjava:dnsjava:3.5.2' + implementation "io.github.nsk90:kstatemachine-jvm:0.22.1" implementation 'net.openid:appauth:0.11.1' implementation "org.apache.commons:commons-collections4:${versions.commonsCollections}" implementation "org.apache.commons:commons-lang3:${versions.commonsLang}" diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt index ef07cd5d0..de9a656b5 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt @@ -30,13 +30,14 @@ import at.bitfire.dav4jvm.DavResource import at.bitfire.dav4jvm.Response import at.bitfire.dav4jvm.exception.HttpException import at.bitfire.dav4jvm.property.* -import at.bitfire.davdroid.network.HttpClient -import at.bitfire.davdroid.network.MemoryCookieStore import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.WebDavDocument import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.network.HttpClient +import at.bitfire.davdroid.network.MemoryCookieStore import at.bitfire.davdroid.ui.webdav.WebdavMountsActivity +import at.bitfire.davdroid.webdav.DavDocumentsProvider.DavDocumentsActor import at.bitfire.davdroid.webdav.cache.HeadResponseCache import dagger.hilt.EntryPoint import dagger.hilt.InstallIn @@ -469,6 +470,7 @@ class DavDocumentsProvider: DocumentsProvider() { } Logger.log.info("Received file info: $fileInfo") + // RandomAccessCallback.Wrapper / StreamingFileDescriptor are responsible for closing httpClient return if ( Build.VERSION.SDK_INT >= 26 && // openProxyFileDescriptor exists since Android 8.0 readAccess && // WebDAV doesn't support random write access natively @@ -477,7 +479,7 @@ class DavDocumentsProvider: DocumentsProvider() { fileInfo.supportsPartial != false // WebDAV server must support random access ) { val accessor = RandomAccessCallback.Wrapper(ourContext, client, url, doc.mimeType, fileInfo, signal) - storageManager.openProxyFileDescriptor(modeFlags, accessor, accessor.callback!!.workerHandler) + storageManager.openProxyFileDescriptor(modeFlags, accessor, accessor.workerHandler) } else { val fd = StreamingFileDescriptor(ourContext, client, url, doc.mimeType, signal) { transferred -> // called when transfer is finished diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallback.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallback.kt index 0b3a26f27..93972d96f 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallback.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallback.kt @@ -26,16 +26,32 @@ import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.ui.NotificationUtils import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible import at.bitfire.davdroid.util.DavUtils +import at.bitfire.davdroid.webdav.RandomAccessCallback.Wrapper.Companion.TIMEOUT_INTERVAL import at.bitfire.davdroid.webdav.cache.MemoryCache import at.bitfire.davdroid.webdav.cache.SegmentedCache import okhttp3.Headers import okhttp3.HttpUrl import okhttp3.MediaType import org.apache.commons.io.FileUtils +import ru.nsk.kstatemachine.DefaultState +import ru.nsk.kstatemachine.Event +import ru.nsk.kstatemachine.FinalState +import ru.nsk.kstatemachine.StateMachine +import ru.nsk.kstatemachine.addFinalState +import ru.nsk.kstatemachine.addInitialState +import ru.nsk.kstatemachine.createStdLibStateMachine +import ru.nsk.kstatemachine.onEntry +import ru.nsk.kstatemachine.onExit +import ru.nsk.kstatemachine.onFinished +import ru.nsk.kstatemachine.processEventBlocking +import ru.nsk.kstatemachine.transition import java.io.InterruptedIOException import java.lang.ref.WeakReference import java.net.HttpURLConnection +import java.util.Timer +import java.util.TimerTask import java.util.logging.Level +import kotlin.concurrent.schedule typealias MemorySegmentCache = MemoryCache> @@ -89,16 +105,11 @@ class RandomAccessCallback private constructor( .setOngoing(true) val notificationTag = url.toString() - private val workerThread = HandlerThread(javaClass.simpleName).apply { start() } - val workerHandler: Handler = Handler(workerThread.looper) - val memoryCache = getMemoryCache(context) val cache = SegmentedCache(PAGE_SIZE, this, memoryCache) - override fun onFsync() { - Logger.log.fine("onFsync") - } + override fun onFsync() { /* not used */ } override fun onGetSize(): Long { Logger.log.fine("onGetFileSize $url") @@ -140,9 +151,6 @@ class RandomAccessCallback private constructor( } override fun onRelease() { - workerThread.quit() - httpClient.close() - notificationManager.cancel(notificationTag, NotificationUtils.NOTIFY_WEBDAV_ACCESS) } @@ -204,34 +212,140 @@ class RandomAccessCallback private constructor( * a [Map]), but is not unregistered anymore. So it stays in the memory until the whole mount * is unloaded. See https://issuetracker.google.com/issues/208788568 * - * Use this wrapper to ensure that all memory is released as soon as [onRelease] is called. + * Use this wrapper to + * + * - ensure that all memory is released as soon as [onRelease] is called, + * - provide timeout functionality: [RandomAccessCallback] will be closed when not + * used for more than [TIMEOUT_INTERVAL] ms and re-created when necessary. + * + * @param httpClient HTTP client – [Wrapper] is responsible to close it */ class Wrapper( - context: Context, - httpClient: HttpClient, - url: HttpUrl, - mimeType: MediaType?, - headResponse: HeadResponse, - cancellationSignal: CancellationSignal? + val context: Context, + val httpClient: HttpClient, + val url: HttpUrl, + val mimeType: MediaType?, + val headResponse: HeadResponse, + val cancellationSignal: CancellationSignal? ): ProxyFileDescriptorCallback() { - var callback: RandomAccessCallback? = RandomAccessCallback(context, httpClient, url, mimeType, headResponse, cancellationSignal) + companion object { + val TIMEOUT_INTERVAL = 15000L + } + + sealed class Events { + object Transfer : Event + object NowIdle : Event + object GoStandby : Event + object Close : Event + } + sealed class States : DefaultState() { + object Active: States() { + object Transferring: States() + object Idle: States() + } + object Standby: States() + object Closed: States(), FinalState + } + val machine = createStdLibStateMachine { + addInitialState(States.Active) { + onEntry { + _callback = RandomAccessCallback(context, httpClient, url, mimeType, headResponse, cancellationSignal) + } + onExit { + _callback?.onRelease() + _callback = null + } + + transition(targetState = States.Standby) + transition(targetState = States.Closed) + + // active has two nested states: transferring (I/O running) and idle (starts timeout timer) + addInitialState(States.Active.Idle) { + val timer: Timer = Timer(true) + var timeout: TimerTask? = null + + onEntry { + timeout = timer.schedule(TIMEOUT_INTERVAL) { + machine.processEventBlocking(Events.GoStandby) + } + } + onExit { + timeout?.cancel() + timeout = null + } + onFinished { + timer.cancel() + } + + transition(targetState = States.Active.Transferring) + } + + addState(States.Active.Transferring) { + transition(targetState = States.Active.Idle) + } + } + + addState(States.Standby) { + transition(targetState = States.Active.Transferring) + transition(targetState = States.Active.Idle) + transition(targetState = States.Closed) + } + + addFinalState(States.Closed) + onFinished { + shutdown() + } + + logger = StateMachine.Logger { message -> + Logger.log.fine(message()) + } + } + + private val workerThread = HandlerThread(javaClass.simpleName).apply { start() } + val workerHandler: Handler = Handler(workerThread.looper) + + private var _callback: RandomAccessCallback? = null + + fun requireCallback(block: (callback: RandomAccessCallback) -> T): T { + machine.processEventBlocking(Events.Transfer) + try { + return block(_callback ?: throw IllegalStateException()) + } finally { + machine.processEventBlocking(Events.NowIdle) + } + } + + + /// states /// - override fun onFsync() = - callback?.onFsync() ?: throw IllegalStateException("Must not be called after onRelease()") + @Synchronized + private fun shutdown() { + httpClient.close() + workerThread.quit() + } + + + /// delegating implementation of ProxyFileDescriptorCallback /// + + @Synchronized + override fun onFsync() { /* not used */ } + @Synchronized override fun onGetSize() = - callback?.onGetSize() ?: throw IllegalStateException("Must not be called after onRelease()") + requireCallback { it.onGetSize() } + @Synchronized override fun onRead(offset: Long, size: Int, data: ByteArray) = - callback?.onRead(offset, size, data) ?: throw IllegalStateException("Must not be called after onRelease()") + requireCallback { it.onRead(offset, size, data) } + @Synchronized override fun onWrite(offset: Long, size: Int, data: ByteArray) = - callback?.onWrite(offset, size, data) ?: throw IllegalStateException("Must not be called after onRelease()") + requireCallback { it.onWrite(offset, size, data) } + @Synchronized override fun onRelease() { - callback?.onRelease() - callback = null + machine.processEventBlocking(Events.Close) } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt index d82d480ce..198620402 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt @@ -28,6 +28,9 @@ import java.io.IOException import java.util.logging.Level import kotlin.concurrent.thread +/** + * @param client HTTP client– [StreamingFileDescriptor] is responsible to close it + */ class StreamingFileDescriptor( val context: Context, val client: HttpClient, @@ -73,6 +76,8 @@ class StreamingFileDescriptor( } catch (e: Exception) { Logger.log.log(Level.INFO, "Couldn't serve file (not necessesarily an error)", e) writeFd.closeWithError(e.message) + } finally { + client.close() } try { -- GitLab From 599c905610d7012a8bb68bb3c5d2aa478f5ec843 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 16 Oct 2023 14:30:26 +0100 Subject: [PATCH 06/23] Replace deprecated menu overrides (#443) * Migrated to menu provider Signed-off-by: Arnau Mora * Removed override Signed-off-by: Arnau Mora * Cleanup Signed-off-by: Arnau Mora * Fixed menus Signed-off-by: Arnau Mora * Minor changes --------- Signed-off-by: Arnau Mora Co-authored-by: Ricki Hirner --- .../at/bitfire/davdroid/ui/AboutActivity.kt | 21 +++-- .../davdroid/ui/AccountListFragment.kt | 33 +++++--- .../bitfire/davdroid/ui/AccountsActivity.kt | 2 +- .../davdroid/ui/account/AccountActivity.kt | 34 ++++++-- .../ui/account/AddressBooksFragment.kt | 46 +++++++---- .../davdroid/ui/account/CalendarsFragment.kt | 47 +++++++---- .../ui/account/CollectionsFragment.kt | 81 ++++++++++--------- .../davdroid/ui/account/WebcalFragment.kt | 38 +++++++-- .../ui/webdav/AddWebdavMountActivity.kt | 25 ++++-- .../ui/webdav/WebdavMountsActivity.kt | 22 +++-- app/src/main/res/menu/activity_about.xml | 4 +- app/src/main/res/menu/activity_account.xml | 3 - app/src/main/res/menu/activity_accounts.xml | 1 - .../res/menu/activity_add_webdav_mount.xml | 3 +- .../main/res/menu/activity_webdav_mounts.xml | 3 +- 15 files changed, 246 insertions(+), 117 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AboutActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AboutActivity.kt index a2079164a..92a6f3cac 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AboutActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AboutActivity.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.core.text.HtmlCompat +import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter @@ -81,14 +82,24 @@ class AboutActivity: AppCompatActivity() { binding.viewpager.adapter = TabsAdapter(supportFragmentManager) binding.tabs.setupWithViewPager(binding.viewpager, false) - } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.activity_about, menu) - return true + addMenuProvider(object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.activity_about, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem) = + when (menuItem.itemId) { + R.id.show_website -> { + showWebsite() + true + } + else -> false + } + }) } - fun showWebsite(item: MenuItem) { + fun showWebsite() { UiUtils.launchUri(this, App.homepageUrl(this)) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountListFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountListFragment.kt index 13d6288f2..462d5e006 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountListFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountListFragment.kt @@ -20,10 +20,13 @@ import android.provider.Settings import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.annotation.AnyThread import androidx.core.content.ContextCompat +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.AndroidViewModel @@ -58,8 +61,6 @@ class AccountListFragment: Fragment() { private var syncStatusSnackbar: Snackbar? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - setHasOptionsMenu(true) - _binding = AccountListBinding.inflate(inflater, container, false) return binding.root } @@ -132,16 +133,28 @@ class AccountListFragment: Fragment() { accountAdapter.submitList(accounts) requireActivity().invalidateOptionsMenu() } - } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) = - inflater.inflate(R.menu.activity_accounts, menu) + requireActivity().addMenuProvider(object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.activity_accounts, menu) + } - override fun onPrepareOptionsMenu(menu: Menu) { - // Show "Sync all" only when there is at least one account - model.accounts.value?.let { accounts -> - menu.findItem(R.id.syncAll).setVisible(accounts.isNotEmpty()) - } + override fun onMenuItemSelected(menuItem: MenuItem) = + when (menuItem.itemId) { + R.id.syncAll -> { + (activity as AccountsActivity).syncAllAccounts() + true + } + else -> false + } + + override fun onPrepareMenu(menu: Menu) { + // Show "Sync all" only when there is at least one account + model.accounts.value?.let { accounts -> + menu.findItem(R.id.syncAll).setVisible(accounts.isNotEmpty()) + } + } + }) } override fun onResume() { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt index 808db922d..5bf41a3c4 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt @@ -109,7 +109,7 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele private fun allAccounts() = AccountManager.get(this).getAccountsByType(getString(R.string.account_type)) - fun syncAllAccounts(item: MenuItem? = null) { + fun syncAllAccounts() { if (Build.VERSION.SDK_INT >= 25) getSystemService()?.reportShortcutUsed(UiUtils.SHORTCUT_SYNC_ALL) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt index 8154948e7..59adc5c56 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt @@ -13,10 +13,12 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import android.view.Menu +import android.view.MenuInflater import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.TooltipCompat +import androidx.core.view.MenuProvider import androidx.fragment.app.FragmentActivity import androidx.lifecycle.* import androidx.viewpager2.adapter.FragmentStateAdapter @@ -105,27 +107,45 @@ class AccountActivity: AppCompatActivity() { SyncWorker.enqueueAllAuthorities(this, model.account) } } - } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.activity_account, menu) - return true + addMenuProvider(object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.activity_account, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem) = + when (menuItem.itemId) { + R.id.settings -> { + openAccountSettings() + true + } + R.id.rename_account -> { + renameAccount() + true + } + R.id.delete_account -> { + deleteAccountDialog() + true + } + else -> false + } + }) } // menu actions - fun openAccountSettings(menuItem: MenuItem) { + fun openAccountSettings() { val intent = Intent(this, SettingsActivity::class.java) intent.putExtra(SettingsActivity.EXTRA_ACCOUNT, model.account) startActivity(intent, null) } - fun renameAccount(menuItem: MenuItem) { + fun renameAccount() { RenameAccountFragment.newInstance(model.account).show(supportFragmentManager, null) } - fun deleteAccount(menuItem: MenuItem) { + fun deleteAccountDialog() { MaterialAlertDialogBuilder(this) .setIcon(R.drawable.ic_error) .setTitle(R.string.account_delete_confirmation_title) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AddressBooksFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AddressBooksFragment.kt index 2a30b1304..67cda108b 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AddressBooksFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AddressBooksFragment.kt @@ -5,7 +5,10 @@ package at.bitfire.davdroid.ui.account import android.content.Intent +import android.os.Bundle import android.view.* +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.fragment.app.FragmentManager import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.davdroid.R @@ -22,26 +25,39 @@ class AddressBooksFragment: CollectionsFragment() { override val noCollectionsStringId = R.string.account_no_address_books - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) = - inflater.inflate(R.menu.carddav_actions, menu) + private val menuProvider = object : CollectionsMenuProvider() { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.carddav_actions, menu) + } - override fun onPrepareOptionsMenu(menu: Menu) { - menu.findItem(R.id.create_address_book).isVisible = model.hasWriteableCollections.value ?: false - super.onPrepareOptionsMenu(menu) - } + override fun onPrepareMenu(menu: Menu) { + super.onPrepareMenu(menu) + menu.findItem(R.id.create_address_book).isVisible = model.hasWriteableCollections.value ?: false + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + if (super.onMenuItemSelected(menuItem)) + return true - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (super.onOptionsItemSelected(item)) - return true + if (menuItem.itemId == R.id.create_address_book) { + val intent = Intent(requireActivity(), CreateAddressBookActivity::class.java) + intent.putExtra(CreateAddressBookActivity.EXTRA_ACCOUNT, accountModel.account) + startActivity(intent) + return true + } - if (item.itemId == R.id.create_address_book) { - val intent = Intent(requireActivity(), CreateAddressBookActivity::class.java) - intent.putExtra(CreateAddressBookActivity.EXTRA_ACCOUNT, accountModel.account) - startActivity(intent) - return true + return false } + } + + override fun onResume() { + super.onResume() + requireActivity().addMenuProvider(menuProvider) + } - return false + override fun onPause() { + super.onPause() + requireActivity().removeMenuProvider(menuProvider) } override fun checkPermissions() { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CalendarsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CalendarsFragment.kt index cbd7916dc..9e58b0bf9 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CalendarsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CalendarsFragment.kt @@ -5,7 +5,10 @@ package at.bitfire.davdroid.ui.account import android.content.Intent +import android.os.Bundle import android.view.* +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.fragment.app.FragmentManager import at.bitfire.davdroid.Constants import at.bitfire.davdroid.util.PermissionUtils @@ -18,26 +21,40 @@ class CalendarsFragment: CollectionsFragment() { override val noCollectionsStringId = R.string.account_no_calendars - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) = - inflater.inflate(R.menu.caldav_actions, menu) + private val menuProvider = object : CollectionsMenuProvider() { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.caldav_actions, menu) + } - override fun onPrepareOptionsMenu(menu: Menu) { - menu.findItem(R.id.create_calendar).isVisible = model.hasWriteableCollections.value ?: false - super.onPrepareOptionsMenu(menu) - } + override fun onPrepareMenu(menu: Menu) { + super.onPrepareMenu(menu) + menu.findItem(R.id.create_calendar).isVisible = model.hasWriteableCollections.value ?: false + } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (super.onOptionsItemSelected(item)) - return true + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + if (super.onMenuItemSelected(menuItem)) { + return true + } - if (item.itemId == R.id.create_calendar) { - val intent = Intent(requireActivity(), CreateCalendarActivity::class.java) - intent.putExtra(CreateCalendarActivity.EXTRA_ACCOUNT, accountModel.account) - startActivity(intent) - return true + if (menuItem.itemId == R.id.create_calendar) { + val intent = Intent(requireActivity(), CreateCalendarActivity::class.java) + intent.putExtra(CreateCalendarActivity.EXTRA_ACCOUNT, accountModel.account) + startActivity(intent) + return true + } + + return false } + } + + override fun onResume() { + super.onResume() + requireActivity().addMenuProvider(menuProvider) + } - return false + override fun onPause() { + super.onPause() + requireActivity().removeMenuProvider(menuProvider) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionsFragment.kt index e845ebc16..e94621f9c 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CollectionsFragment.kt @@ -13,6 +13,9 @@ import android.provider.CalendarContract import android.provider.ContactsContract import android.view.* import android.widget.PopupMenu +import androidx.annotation.CallSuper +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.activityViewModels @@ -66,11 +69,6 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - abstract val noCollectionsStringId: Int override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @@ -86,17 +84,17 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - model.isRefreshing.observe(viewLifecycleOwner, Observer { nowRefreshing -> + model.isRefreshing.observe(viewLifecycleOwner) { nowRefreshing -> binding.swipeRefresh.isRefreshing = nowRefreshing - }) - model.hasWriteableCollections.observe(viewLifecycleOwner, Observer { + } + model.hasWriteableCollections.observe(viewLifecycleOwner) { requireActivity().invalidateOptionsMenu() - }) - model.collectionColors.observe(viewLifecycleOwner, Observer { colors: List -> + } + model.collectionColors.observe(viewLifecycleOwner) { colors: List -> val realColors = colors.filterNotNull() if (realColors.isNotEmpty()) binding.swipeRefresh.setColorSchemeColors(*realColors.toIntArray()) - }) + } binding.swipeRefresh.setOnRefreshListener(this) val updateProgress = Observer { @@ -128,11 +126,11 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList val adapter = createAdapter() binding.list.layoutManager = LinearLayoutManager(requireActivity()) binding.list.adapter = adapter - model.collections.observe(viewLifecycleOwner, Observer { data -> + model.collections.observe(viewLifecycleOwner) { data -> lifecycleScope.launch { adapter.submitData(data) } - }) + } adapter.addLoadStateListener { loadStates -> if (loadStates.refresh is LoadState.NotLoading) { if (adapter.itemCount > 0) { @@ -148,31 +146,6 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList binding.noCollections.setText(noCollectionsStringId) } - override fun onPrepareOptionsMenu(menu: Menu) { - menu.findItem(R.id.showOnlyPersonal).let { showOnlyPersonal -> - accountModel.showOnlyPersonal.value?.let { value -> - showOnlyPersonal.isChecked = value - } - accountModel.showOnlyPersonalWritable.value?.let { writable -> - showOnlyPersonal.isEnabled = writable - } - } - } - - override fun onOptionsItemSelected(item: MenuItem) = - when (item.itemId) { - R.id.refresh -> { - onRefresh() - true - } - R.id.showOnlyPersonal -> { - accountModel.toggleShowOnlyPersonal() - true - } - else -> - false - } - override fun onRefresh() { model.refresh() } @@ -223,6 +196,38 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList } + abstract inner class CollectionsMenuProvider : MenuProvider { + abstract override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) + + @CallSuper + override fun onPrepareMenu(menu: Menu) { + menu.findItem(R.id.showOnlyPersonal).let { showOnlyPersonal -> + accountModel.showOnlyPersonal.value?.let { value -> + showOnlyPersonal.isChecked = value + } + accountModel.showOnlyPersonalWritable.value?.let { writable -> + showOnlyPersonal.isEnabled = writable + } + } + } + + @CallSuper + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.refresh -> { + onRefresh() + true + } + R.id.showOnlyPersonal -> { + accountModel.toggleShowOnlyPersonal() + true + } + else -> + false + } + } + } + class CollectionPopupListener( private val accountModel: AccountActivity.Model, private val item: Collection, diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt index 56a1db3fd..a3501070d 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt @@ -60,6 +60,32 @@ class WebcalFragment: CollectionsFragment() { } } + private val menuProvider = object : CollectionsMenuProvider() { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.caldav_actions, menu) + } + + override fun onPrepareMenu(menu: Menu) { + super.onPrepareMenu(menu) + menu.findItem(R.id.create_calendar).isVisible = false + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + if (super.onMenuItemSelected(menuItem)) { + return true + } + + if (menuItem.itemId == R.id.create_calendar) { + val intent = Intent(requireActivity(), CreateCalendarActivity::class.java) + intent.putExtra(CreateCalendarActivity.EXTRA_ACCOUNT, accountModel.account) + startActivity(intent) + return true + } + + return false + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -68,12 +94,14 @@ class WebcalFragment: CollectionsFragment() { }) } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) = - inflater.inflate(R.menu.caldav_actions, menu) + override fun onResume() { + super.onResume() + requireActivity().addMenuProvider(menuProvider) + } - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - menu.findItem(R.id.create_calendar).isVisible = false + override fun onPause() { + super.onPause() + requireActivity().removeMenuProvider(menuProvider) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt index 72f57a8c0..3b5a9adb4 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt @@ -7,24 +7,26 @@ package at.bitfire.davdroid.ui.webdav import android.content.Context import android.os.Bundle import android.view.Menu +import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.activity.viewModels import androidx.annotation.WorkerThread import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.MenuProvider import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.lifecycleScope import at.bitfire.dav4jvm.DavResource import at.bitfire.dav4jvm.UrlUtils import at.bitfire.davdroid.App -import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.R import at.bitfire.davdroid.databinding.ActivityAddWebdavMountBinding import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.db.WebDavMount import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.ui.UiUtils import at.bitfire.davdroid.webdav.CredentialsStore import at.bitfire.davdroid.webdav.DavDocumentsProvider @@ -66,14 +68,25 @@ class AddWebdavMountActivity: AppCompatActivity() { binding.addMount.setOnClickListener { validate() } - } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.activity_add_webdav_mount, menu) - return true + addMenuProvider(object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.activity_add_webdav_mount, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.help -> { + onShowHelp() + true + } + else -> false + } + } + }) } - fun onShowHelp(item: MenuItem) { + fun onShowHelp() { UiUtils.launchUri(this, App.homepageUrl(this).buildUpon().appendPath("tested-with").build()) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt index 3fdd02509..6d3a0ae86 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt @@ -15,6 +15,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.text.HtmlCompat +import androidx.core.view.MenuProvider import androidx.lifecycle.* import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager @@ -88,14 +89,25 @@ class WebdavMountsActivity: AppCompatActivity() { binding.add.setOnClickListener { startActivity(Intent(this, AddWebdavMountActivity::class.java)) } - } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.activity_webdav_mounts, menu) - return true + addMenuProvider(object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.activity_webdav_mounts, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.help -> { + onShowHelp() + true + } + else -> false + } + } + }) } - fun onShowHelp(item: MenuItem) { + fun onShowHelp() { UiUtils.launchUri(this, helpUrl()) } diff --git a/app/src/main/res/menu/activity_about.xml b/app/src/main/res/menu/activity_about.xml index 74d68b07e..9a6fa9530 100644 --- a/app/src/main/res/menu/activity_about.xml +++ b/app/src/main/res/menu/activity_about.xml @@ -3,9 +3,9 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + app:showAsAction="ifRoom" /> \ No newline at end of file diff --git a/app/src/main/res/menu/activity_account.xml b/app/src/main/res/menu/activity_account.xml index 3ad391ccb..c64e8cfd5 100644 --- a/app/src/main/res/menu/activity_account.xml +++ b/app/src/main/res/menu/activity_account.xml @@ -7,17 +7,14 @@ diff --git a/app/src/main/res/menu/activity_accounts.xml b/app/src/main/res/menu/activity_accounts.xml index f173669d3..05a0244f5 100644 --- a/app/src/main/res/menu/activity_accounts.xml +++ b/app/src/main/res/menu/activity_accounts.xml @@ -5,7 +5,6 @@ \ No newline at end of file diff --git a/app/src/main/res/menu/activity_add_webdav_mount.xml b/app/src/main/res/menu/activity_add_webdav_mount.xml index 520e126f0..5c1f92216 100644 --- a/app/src/main/res/menu/activity_add_webdav_mount.xml +++ b/app/src/main/res/menu/activity_add_webdav_mount.xml @@ -6,7 +6,6 @@ android:id="@+id/help" android:icon="@drawable/ic_help" android:title="@string/help" - app:showAsAction="always" - android:onClick="onShowHelp" /> + app:showAsAction="always" /> \ No newline at end of file diff --git a/app/src/main/res/menu/activity_webdav_mounts.xml b/app/src/main/res/menu/activity_webdav_mounts.xml index 520e126f0..5c1f92216 100644 --- a/app/src/main/res/menu/activity_webdav_mounts.xml +++ b/app/src/main/res/menu/activity_webdav_mounts.xml @@ -6,7 +6,6 @@ android:id="@+id/help" android:icon="@drawable/ic_help" android:title="@string/help" - app:showAsAction="always" - android:onClick="onShowHelp" /> + app:showAsAction="always" /> \ No newline at end of file -- GitLab From 5ae70cb5d040b00208c01215f0cdd0000d9cabac Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 16 Oct 2023 14:31:17 +0100 Subject: [PATCH 07/23] BatteryOptimizationIntroFragment: use contract instead of onActivityResult (#444) * Using result launcher Signed-off-by: Arnau Mora * Minor re-ordering --------- Signed-off-by: Arnau Mora Co-authored-by: Ricki Hirner --- .../ui/intro/BatteryOptimizationsFragment.kt | 68 +++++++++++-------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt index c6a8df5da..f58f08ba4 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt @@ -5,6 +5,7 @@ package at.bitfire.davdroid.ui.intro import android.annotation.SuppressLint +import android.app.Application import android.content.Context import android.content.Intent import android.net.Uri @@ -14,12 +15,13 @@ import android.os.PowerManager import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContract import androidx.core.content.getSystemService import androidx.databinding.ObservableBoolean import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import at.bitfire.davdroid.App import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.R @@ -34,7 +36,6 @@ import dagger.hilt.InstallIn import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.components.ActivityComponent import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.multibindings.IntoSet import org.apache.commons.text.WordUtils import java.util.* @@ -43,12 +44,13 @@ import javax.inject.Inject @AndroidEntryPoint class BatteryOptimizationsFragment: Fragment() { - companion object { - const val REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = 0 - } - val model by viewModels() + private val ignoreBatteryOptimizationsResultLauncher = + registerForActivityResult(IgnoreBatteryOptimizationsContract) { + model.checkWhitelisted() + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { val binding = IntroBatteryOptimizationsBinding.inflate(inflater, container, false) @@ -58,10 +60,7 @@ class BatteryOptimizationsFragment: Fragment() { model.shouldBeWhitelisted.observe(viewLifecycleOwner) { shouldBeWhitelisted -> @SuppressLint("BatteryLife") if (shouldBeWhitelisted && !model.isWhitelisted.value!!) - startActivityForResult(Intent( - android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, - Uri.parse("package:" + BuildConfig.APPLICATION_ID) - ), REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + ignoreBatteryOptimizationsResultLauncher.launch(BuildConfig.APPLICATION_ID) } binding.batteryText.text = getString(R.string.intro_battery_text, getString(R.string.app_name)) @@ -78,12 +77,6 @@ class BatteryOptimizationsFragment: Fragment() { return binding.root } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) - model.checkWhitelisted() - } - override fun onResume() { super.onResume() model.checkWhitelisted() @@ -92,9 +85,9 @@ class BatteryOptimizationsFragment: Fragment() { @HiltViewModel class Model @Inject constructor( - @ApplicationContext val context: Context, + application: Application, val settings: SettingsManager - ): ViewModel() { + ): AndroidViewModel(application) { companion object { @@ -163,7 +156,7 @@ class BatteryOptimizationsFragment: Fragment() { } fun checkWhitelisted() { - val whitelisted = isWhitelisted(context) + val whitelisted = isWhitelisted(getApplication()) isWhitelisted.value = whitelisted shouldBeWhitelisted.value = whitelisted @@ -175,6 +168,21 @@ class BatteryOptimizationsFragment: Fragment() { } + @SuppressLint("BatteryLife") + object IgnoreBatteryOptimizationsContract: ActivityResultContract() { + override fun createIntent(context: Context, input: String): Intent { + return Intent( + android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:$input") + ) + } + + override fun parseResult(resultCode: Int, intent: Intent?): Unit? { + return null + } + } + + @Module @InstallIn(ActivityComponent::class) abstract class BatteryOptimizationsFragmentModule { @@ -187,17 +195,17 @@ class BatteryOptimizationsFragment: Fragment() { ): IntroFragmentFactory { override fun getOrder(context: Context) = - // show fragment when: - // 1. DAVx5 is not whitelisted yet and "don't show anymore" has not been clicked, and/or - // 2a. evil manufacturer AND - // 2b. "don't show anymore" has not been clicked - if ( - (!Model.isWhitelisted(context) && settingsManager.getBooleanOrNull(HINT_BATTERY_OPTIMIZATIONS) != false) || - (Model.manufacturerWarning && settingsManager.getBooleanOrNull(HINT_AUTOSTART_PERMISSION) != false) - ) - 100 - else - IntroFragmentFactory.DONT_SHOW + // show fragment when: + // 1. DAVx5 is not whitelisted yet and "don't show anymore" has not been clicked, and/or + // 2a. evil manufacturer AND + // 2b. "don't show anymore" has not been clicked + if ( + (!Model.isWhitelisted(context) && settingsManager.getBooleanOrNull(HINT_BATTERY_OPTIMIZATIONS) != false) || + (Model.manufacturerWarning && settingsManager.getBooleanOrNull(HINT_AUTOSTART_PERMISSION) != false) + ) + 100 + else + IntroFragmentFactory.DONT_SHOW override fun create() = BatteryOptimizationsFragment() } -- GitLab From 8ffed42eb936592ee84821eba1fbc87a3794cb83 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 17 Oct 2023 10:17:17 +0200 Subject: [PATCH 08/23] PermissionsIntroFragment: take jtx Board and tasks.org permissions into account (#450) --- .../at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt index 0588aefce..98df793fd 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt @@ -27,7 +27,10 @@ class PermissionsIntroFragment : Fragment() { override fun getOrder(context: Context): Int { // show PermissionsFragment as intro fragment when no permissions are granted - val permissions = CONTACT_PERMISSIONS + CALENDAR_PERMISSIONS + TaskProvider.PERMISSIONS_OPENTASKS + val permissions = CONTACT_PERMISSIONS + CALENDAR_PERMISSIONS + + TaskProvider.PERMISSIONS_JTX + + TaskProvider.PERMISSIONS_OPENTASKS + + TaskProvider.PERMISSIONS_TASKS_ORG return if (PermissionUtils.haveAnyPermission(context, permissions)) IntroFragmentFactory.DONT_SHOW else -- GitLab From 58d4a9f663054532667beafcef49c0cf9467032c Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 17 Oct 2023 18:50:42 +0200 Subject: [PATCH 09/23] Make all IntroFragments appear at first start (#452) * IntroFragments: use (factory,order) List instead of (order,factory) Map to store them * Adapt OpenSourceFragment order --- .../bitfire/davdroid/ui/AccountsActivity.kt | 5 ++-- .../ui/intro/BatteryOptimizationsFragment.kt | 2 +- .../davdroid/ui/intro/IntroActivity.kt | 25 +++++++++++-------- .../davdroid/ui/intro/IntroFragmentFactory.kt | 2 +- .../davdroid/ui/intro/OpenSourceFragment.kt | 4 +-- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt index 5bf41a3c4..8d3053328 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt @@ -54,11 +54,10 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele super.onCreate(savedInstanceState) if (savedInstanceState == null) { + // use a separate thread to check whether IntroActivity should be shown CoroutineScope(Dispatchers.Default).launch { - // use a separate thread to check whether IntroActivity should be shown - if (IntroActivity.shouldShowIntroActivity(this@AccountsActivity)) { + if (IntroActivity.shouldShowIntroActivity(this@AccountsActivity)) introActivityLauncher.launch(null) - } } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt index f58f08ba4..761e1f45e 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt @@ -210,4 +210,4 @@ class BatteryOptimizationsFragment: Fragment() { override fun create() = BatteryOptimizationsFragment() } -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt index 914c3ac29..535bebb17 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt @@ -10,6 +10,7 @@ import android.content.Intent import android.os.Bundle import androidx.activity.result.contract.ActivityResultContract import androidx.activity.addCallback +import androidx.annotation.WorkerThread import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import at.bitfire.davdroid.R @@ -33,12 +34,11 @@ class IntroActivity: AppIntro2() { companion object { + @WorkerThread fun shouldShowIntroActivity(activity: Activity): Boolean { val factories = EntryPointAccessors.fromActivity(activity, IntroActivityEntryPoint::class.java).introFragmentFactories() return factories.any { - val order = it.getOrder(activity) - Logger.log.fine("Found intro fragment factory ${it::class.java} with order $order") - order > 0 + it.getOrder(activity) > 0 } } @@ -46,19 +46,24 @@ class IntroActivity: AppIntro2() { private var currentSlide = 0 - @Inject lateinit var introFragmentFactories: Set<@JvmSuppressWildcards IntroFragmentFactory> - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val factoriesWithOrder = introFragmentFactories - .associateBy { it.getOrder(this) } - .filterKeys { it != IntroFragmentFactory.DONT_SHOW } + val factories = EntryPointAccessors.fromActivity(this, IntroActivityEntryPoint::class.java).introFragmentFactories() + for (factory in factories) + Logger.log.fine("Found intro fragment factory ${factory::class.java} with order ${factory.getOrder(this)}") + + val factoriesWithOrder = factories + .associateWith { it.getOrder(this) } + .filterValues { it != IntroFragmentFactory.DONT_SHOW } - val anyPositiveOrder = factoriesWithOrder.keys.any { it > 0 } + val anyPositiveOrder = factoriesWithOrder.values.any { it > 0 } if (anyPositiveOrder) { - for ((_, factory) in factoriesWithOrder.toSortedMap()) + val factoriesSortedByOrder = factoriesWithOrder + .toList() + .sortedBy { (_, v) -> v } // sort by value (= getOrder()) + for ((factory, _) in factoriesSortedByOrder) addSlide(factory.create()) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroFragmentFactory.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroFragmentFactory.kt index 3e6493ff3..3488f84de 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroFragmentFactory.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroFragmentFactory.kt @@ -22,7 +22,7 @@ interface IntroFragmentFactory { * @return Order with which an instance of this fragment type shall be created and shown. Possible values: * * * <0: only show the fragment when there is at least one other fragment with positive order (lower numbers are shown first) - * * 0: don't show the fragment + * * [DONT_SHOW] (0): don't show the fragment * * ≥0: show the fragment (lower numbers are shown first) */ fun getOrder(context: Context): Int diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourceFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourceFragment.kt index 30f01de01..020482690 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourceFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourceFragment.kt @@ -21,7 +21,6 @@ import at.bitfire.davdroid.ui.UiUtils import at.bitfire.davdroid.ui.intro.OpenSourceFragment.Model.Companion.SETTING_NEXT_DONATION_POPUP import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject @AndroidEntryPoint @@ -48,7 +47,6 @@ class OpenSourceFragment: Fragment() { @HiltViewModel class Model @Inject constructor( - @ApplicationContext val context: Context, val settings: SettingsManager ): ViewModel() { @@ -76,7 +74,7 @@ class OpenSourceFragment: Fragment() { override fun getOrder(context: Context) = if (System.currentTimeMillis() > (settingsManager.getLongOrNull(SETTING_NEXT_DONATION_POPUP) ?: 0)) - 100 + 500 else 0 -- GitLab From 52747e632fbedcb21b578918c6802bb32cc2a6f9 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 18 Oct 2023 14:44:16 +0200 Subject: [PATCH 10/23] LoginActivity: add Nextcloud Login Flow (bitfireAT/davx5#403) * Replace onActivityResult by contract * Add Nextcloud option to default login screen * Decouple NextcloudLoginFlowComposable from model * UI and model changes * Single-line URL field * Add progress indicator and other secondary UI --- .../setup/DefaultLoginCredentialsFragment.kt | 12 +- .../ui/setup/DefaultLoginCredentialsModel.kt | 1 + .../ui/setup/NextcloudLoginFlowFragment.kt | 279 ++++++++++++++---- .../res/layout/login_credentials_fragment.xml | 17 +- app/src/main/res/values/strings.xml | 8 + 5 files changed, 252 insertions(+), 65 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsFragment.kt index 27bca24a6..1f2b53382 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsFragment.kt @@ -71,10 +71,11 @@ class DefaultLoginCredentialsFragment : Fragment() { v.login.setOnClickListener { _ -> if (validate()) { val nextFragment = - if (model.loginGoogle.value == true) - GoogleLoginFragment() - else - DetectConfigurationFragment() + when { + model.loginGoogle.value == true -> GoogleLoginFragment() + model.loginNextcloud.value == true -> NextcloudLoginFlowFragment() + else -> DetectConfigurationFragment() + } parentFragmentManager.beginTransaction() .replace(android.R.id.content, nextFragment, null) @@ -204,7 +205,8 @@ class DefaultLoginCredentialsFragment : Fragment() { } } - model.loginGoogle.value == true -> { + // some login methods don't require further input → always valid + model.loginGoogle.value == true || model.loginNextcloud.value == true -> { valid = true } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsModel.kt index 30c9ed2d1..5edb235be 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsModel.kt @@ -29,6 +29,7 @@ class DefaultLoginCredentialsModel(app: Application): AndroidViewModel(app) { val loginWithUrlAndUsername = MutableLiveData(false) val loginAdvanced = MutableLiveData(false) val loginGoogle = MutableLiveData(false) + val loginNextcloud = MutableLiveData(false) val baseUrl = MutableLiveData() val baseUrlError = MutableLiveData() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginFlowFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginFlowFragment.kt index 3e9a25403..423a4499a 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginFlowFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginFlowFragment.kt @@ -7,16 +7,38 @@ package at.bitfire.davdroid.ui.setup import android.annotation.SuppressLint import android.app.Application import android.content.Intent -import android.net.Uri import android.os.Bundle import android.provider.Browser import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.browser.customtabs.CustomTabsIntent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Button +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.core.os.bundleOf import androidx.fragment.app.Fragment @@ -24,14 +46,15 @@ import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope import at.bitfire.dav4jvm.exception.DavException import at.bitfire.dav4jvm.exception.HttpException import at.bitfire.davdroid.R import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.network.HttpClient -import at.bitfire.davdroid.ui.DebugInfoActivity import at.bitfire.davdroid.ui.UiUtils.haveCustomTabs +import com.google.accompanist.themeadapter.material.MdcTheme import com.google.android.material.snackbar.Snackbar import dagger.Binds import dagger.Module @@ -39,11 +62,11 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntKey import dagger.multibindings.IntoMap -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody @@ -51,14 +74,15 @@ import okhttp3.RequestBody.Companion.toRequestBody import org.json.JSONObject import java.net.HttpURLConnection import java.net.URI +import java.util.logging.Level import javax.inject.Inject class NextcloudLoginFlowFragment: Fragment() { companion object { - const val LOGIN_FLOW_V1_PATH = "/index.php/login/flow" - const val LOGIN_FLOW_V2_PATH = "/index.php/login/v2" + const val LOGIN_FLOW_V1_PATH = "index.php/login/flow" + val LOGIN_FLOW_V2_PATH = "index.php/login/v2" /** Set this to 1 to indicate that Login Flow shall be used. */ const val EXTRA_LOGIN_FLOW = "loginFlow" @@ -66,31 +90,47 @@ class NextcloudLoginFlowFragment: Fragment() { /** Path to DAV endpoint (e.g. `/remote.php/dav`). Will be appended to the * server URL returned by Login Flow without further processing. */ const val EXTRA_DAV_PATH = "davPath" - - const val REQUEST_BROWSER = 0 } val loginModel by activityViewModels() - val loginFlowModel by viewModels() + val model by viewModels() + + val checkResultCallback = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + val davPath = requireActivity().intent.getStringExtra(EXTRA_DAV_PATH) + model.checkResult(davPath) + } @SuppressLint("SetJavaScriptEnabled") override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - val view = View(requireActivity()) - - val entryUrl = requireActivity().intent.data ?: throw IllegalArgumentException("Intent data must be set to Login Flow URL") - Logger.log.info("Using Login Flow entry point: $entryUrl") + val entryUrl = requireActivity().intent.data?.toString()?.toHttpUrlOrNull() + + val view = ComposeView(requireActivity()).apply { + setContent { + MdcTheme { + NextcloudLoginComposable( + onStart = { url -> + model.start(url) + }, + entryUrl = entryUrl, + inProgress = model.inProgress.observeAsState(false), + error = model.error.observeAsState() + ) + } + } + } - loginFlowModel.loginUrl.observe(viewLifecycleOwner) { loginUrl -> + model.loginUrl.observe(viewLifecycleOwner) { loginUrl -> if (loginUrl == null) return@observe val loginUri = loginUrl.toUri() // reset URL so that the browser isn't shown another time - loginFlowModel.loginUrl.value = null + model.loginUrl.value = null if (haveCustomTabs(requireActivity())) { // Custom Tabs are available + @Suppress("DEPRECATION") val browser = CustomTabsIntent.Builder() .setToolbarColor(resources.getColor(R.color.primaryColor)) .build() @@ -99,31 +139,23 @@ class NextcloudLoginFlowFragment: Fragment() { Browser.EXTRA_HEADERS, bundleOf("Accept-Language" to Locale.current.toLanguageTag()) ) - startActivityForResult(browser.intent, REQUEST_BROWSER, browser.startAnimationBundle) - + checkResultCallback.launch(browser.intent) } else { // fallback: launch normal browser val browser = Intent(Intent.ACTION_VIEW, loginUri) browser.addCategory(Intent.CATEGORY_BROWSABLE) if (browser.resolveActivity(requireActivity().packageManager) != null) - startActivityForResult(browser, REQUEST_BROWSER) + checkResultCallback.launch(browser) else Snackbar.make(view, getString(R.string.install_browser), Snackbar.LENGTH_INDEFINITE).show() } } - loginFlowModel.error.observe(viewLifecycleOwner) { exception -> - Snackbar.make(requireView(), exception.toString(), Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.exception_show_details) { - val intent = DebugInfoActivity.IntentBuilder(requireActivity()) - .withCause(exception) - .build() - startActivity(intent) - } - .show() - } + model.loginData.observe(viewLifecycleOwner) { loginData -> + if (loginData == null) + return@observe + val (baseUri, credentials) = loginData - loginFlowModel.loginData.observe(viewLifecycleOwner) { (baseUri, credentials) -> // continue to next fragment loginModel.baseURI = baseUri loginModel.credentials = credentials @@ -131,38 +163,33 @@ class NextcloudLoginFlowFragment: Fragment() { .replace(android.R.id.content, DetectConfigurationFragment(), null) .addToBackStack(null) .commit() + + // reset loginData so that we can go back + model.loginData.value = null } - // start Login Flow - loginFlowModel.setUrl(entryUrl) + if (savedInstanceState == null && entryUrl != null) + model.start(entryUrl) return view } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode != REQUEST_BROWSER) - return - - val davPath = requireActivity().intent.getStringExtra(EXTRA_DAV_PATH) - loginFlowModel.checkResult(davPath) - } - /** * Implements Login Flow v2. * * @see https://docs.nextcloud.com/server/20/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2 */ - class LoginFlowModel(app: Application): AndroidViewModel(app) { - - val error = MutableLiveData() + class Model(app: Application): AndroidViewModel(app) { val loginUrl = MutableLiveData() + val error = MutableLiveData() val httpClient by lazy { HttpClient.Builder(getApplication()) - .setForeground(true) - .build() + .setForeground(true) + .build() } + val inProgress = MutableLiveData(false) var pollUrl: HttpUrl? = null var token: String? = null @@ -174,20 +201,30 @@ class NextcloudLoginFlowFragment: Fragment() { } + /** + * Starts the Login Flow. + * + * @param entryUrl entryURL: either a Login Flow path (ending with [LOGIN_FLOW_V1_PATH] or [LOGIN_FLOW_V2_PATH]), + * or another URL which is treated as Nextcloud root URL. In this case, [LOGIN_FLOW_V2_PATH] is appended. + */ @UiThread - fun setUrl(entryUri: Uri) { - val entryUrl = entryUri.toString() - val v2Url = - if (entryUrl.endsWith(LOGIN_FLOW_V1_PATH)) - // got Login Flow v1 URL, rewrite to v2 - entryUrl.removeSuffix(LOGIN_FLOW_V1_PATH) + LOGIN_FLOW_V2_PATH - else - entryUrl + fun start(entryUrl: HttpUrl) { + inProgress.value = true + error.value = null + + var entryUrlStr = entryUrl.toString() + if (entryUrlStr.endsWith(LOGIN_FLOW_V1_PATH)) + // got Login Flow v1 URL, rewrite to v2 + entryUrlStr = entryUrlStr.removeSuffix(LOGIN_FLOW_V1_PATH) + + val v2Url = entryUrlStr.toHttpUrl().newBuilder() + .addPathSegments(LOGIN_FLOW_V2_PATH) + .build() // send POST request and process JSON reply - CoroutineScope(Dispatchers.IO).launch { + viewModelScope.launch(Dispatchers.IO) { try { - val json = postForJson(v2Url.toHttpUrl(), "".toRequestBody()) + val json = postForJson(v2Url, "".toRequestBody()) // login URL loginUrl.postValue(json.getString("login")) @@ -198,7 +235,10 @@ class NextcloudLoginFlowFragment: Fragment() { token = poll.getString("token") } } catch (e: Exception) { - error.postValue(e) + Logger.log.log(Level.WARNING, "Couldn't obtain login URL", e) + error.postValue(getApplication().getString(R.string.login_nextcloud_login_flow_no_login_url)) + } finally { + inProgress.postValue(false) } } } @@ -208,7 +248,7 @@ class NextcloudLoginFlowFragment: Fragment() { val pollUrl = pollUrl ?: return val token = token ?: return - CoroutineScope(Dispatchers.IO).launch { + viewModelScope.launch(Dispatchers.IO) { try { val json = postForJson(pollUrl, "token=$token".toRequestBody("application/x-www-form-urlencoded".toMediaType())) val serverUrl = json.getString("server") @@ -221,11 +261,12 @@ class NextcloudLoginFlowFragment: Fragment() { URI.create(serverUrl) loginData.postValue(Pair( - baseUri, - Credentials(loginName, appPassword) + baseUri, + Credentials(loginName, appPassword) )) } catch (e: Exception) { - error.postValue(e) + Logger.log.log(Level.WARNING, "Polling login URL failed", e) + error.postValue(getApplication().getString(R.string.login_nextcloud_login_flow_no_login_data)) } } } @@ -259,7 +300,7 @@ class NextcloudLoginFlowFragment: Fragment() { class Factory @Inject constructor(): LoginCredentialsFragmentFactory { override fun getFragment(intent: Intent) = - if (intent.hasExtra(EXTRA_LOGIN_FLOW)) + if (intent.hasExtra(EXTRA_LOGIN_FLOW) && intent.data != null) NextcloudLoginFlowFragment() else null @@ -275,4 +316,124 @@ class NextcloudLoginFlowFragment: Fragment() { abstract fun factory(impl: Factory): LoginCredentialsFragmentFactory } +} + + +@Composable +fun NextcloudLoginComposable( + entryUrl: HttpUrl?, + inProgress: State, + error: State, + onStart: (HttpUrl) -> Unit +) { + Column { + if (inProgress.value) + LinearProgressIndicator( + color = MaterialTheme.colors.secondary, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + + Column(modifier = Modifier.padding(8.dp)) { + Text( + stringResource(R.string.login_nextcloud_login_with_nextcloud), + style = MaterialTheme.typography.h5 + ) + NextcloudLoginFlowComposable( + providedEntryUrl = entryUrl, + inProgress = inProgress, + error = error, + onStart = onStart + ) + } + } +} + + +@Composable +fun NextcloudLoginFlowComposable( + providedEntryUrl: HttpUrl?, + inProgress: State, + error: State, + onStart: ((HttpUrl) -> Unit) +) { + Column { + Text( + stringResource(R.string.login_nextcloud_login_flow), + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(top = 16.dp) + ) + Text( + stringResource(R.string.login_nextcloud_login_flow_text), + modifier = Modifier.padding(vertical = 8.dp) + ) + + val entryUrlStr = remember { mutableStateOf(providedEntryUrl?.toString() ?: "") } + val entryUrl = remember { mutableStateOf(providedEntryUrl) } + OutlinedTextField(entryUrlStr.value, + onValueChange = { newUrlStr -> + entryUrlStr.value = newUrlStr + + entryUrl.value = try { + val withScheme = + if (!newUrlStr.startsWith("http://", true) && !newUrlStr.startsWith("https://", true)) + "https://$newUrlStr" + else + newUrlStr + withScheme.toHttpUrl() + } catch (e: IllegalArgumentException) { + null + } + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + readOnly = inProgress.value, + label = { + Text(stringResource(R.string.login_nextcloud_login_flow_server_address)) + }, + placeholder = { + Text("cloud.example.com") + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Go + ), + keyboardActions = KeyboardActions( + onGo = { + entryUrl.value?.let(onStart) + } + ), + singleLine = true + ) + + Button( + onClick = { + entryUrl.value?.let(onStart) + }, + enabled = entryUrl.value != null && !inProgress.value + ) { + Text(stringResource(R.string.login_nextcloud_login_flow_sign_in)) + } + + error.value?.let { msg -> + Text( + msg, + color = MaterialTheme.colors.error, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + } +} + +@Composable +@Preview +fun NextcloudLoginFlowComposable_PreviewWithError() { + NextcloudLoginFlowComposable( + providedEntryUrl = null, + inProgress = remember { mutableStateOf(true) }, + error = remember { mutableStateOf("Something wrong happened") }, + onStart = { } + ) } \ No newline at end of file diff --git a/app/src/main/res/layout/login_credentials_fragment.xml b/app/src/main/res/layout/login_credentials_fragment.xml index 8b7a93fb9..47fc82990 100644 --- a/app/src/main/res/layout/login_credentials_fragment.xml +++ b/app/src/main/res/layout/login_credentials_fragment.xml @@ -314,16 +314,31 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6ac45cec3..a64054e88 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -297,6 +297,14 @@ Privacy policy for details.]]> Google API Services User Data Policy, including the Limited Use requirements.]]> Couldn\'t obtain authorization code + Nextcloud + Login with Nextcloud + Login Flow + This will start the Nextcloud Login Flow in a Web browser. + Nextcloud server address + Sign in + Couldn\'t obtain login URL + Couldn\'t obtain login data Configuration detection Please wait, querying server… -- GitLab From b26ae345cd7d159920e500ab94e5b89c197a146c Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 18 Oct 2023 15:08:43 +0200 Subject: [PATCH 11/23] Nextcloud: pre-select contact group method (CATEGORIES) (bitfireAT/davx5#410) * LoginActivity: refactor menu to MenuProvider; LoginModel: add contact group type * Take LoginModel group method into account when creating the account; Nextcloud login: set preferred contact group type --- .../ui/setup/AccountDetailsFragment.kt | 26 +++++++++++++---- .../davdroid/ui/setup/LoginActivity.kt | 28 ++++++++++++------- .../bitfire/davdroid/ui/setup/LoginModel.kt | 10 ++++--- .../ui/setup/NextcloudLoginFlowFragment.kt | 2 ++ app/src/main/res/menu/activity_login.xml | 6 ++-- 5 files changed, 48 insertions(+), 24 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index aa2504b44..2376be214 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -19,7 +19,11 @@ import android.widget.ArrayAdapter import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels -import androidx.lifecycle.* +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R import at.bitfire.davdroid.databinding.LoginAccountDetailsBinding @@ -101,10 +105,10 @@ class AccountDetailsFragment : Fragment() { v.createAccount.visibility = View.GONE model.createAccount( - name, - loginModel.credentials, - config, - GroupMethod.valueOf(groupMethodName) + name, + loginModel.credentials, + config, + GroupMethod.valueOf(groupMethodName) ).observe(viewLifecycleOwner, Observer { success -> if (success) { // close Create account activity @@ -126,6 +130,7 @@ class AccountDetailsFragment : Fragment() { val forcedGroupMethod = settings.getString(AccountSettings.KEY_CONTACT_GROUP_METHOD)?.let { GroupMethod.valueOf(it) } if (forcedGroupMethod != null) { + // contact group type forced by settings v.contactGroupMethod.isEnabled = false for ((i, method) in resources.getStringArray(R.array.settings_contact_group_method_values).withIndex()) { if (method == forcedGroupMethod.name) { @@ -133,8 +138,17 @@ class AccountDetailsFragment : Fragment() { break } } - } else + } else { + // contact group type selectable v.contactGroupMethod.isEnabled = true + for ((i, method) in resources.getStringArray(R.array.settings_contact_group_method_values).withIndex()) { + // take suggestion from detection process into account + if (method == loginModel.suggestedGroupMethod.name) { + v.contactGroupMethod.setSelection(i) + break + } + } + } return v.root } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginActivity.kt index e03c39899..3925fcc13 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginActivity.kt @@ -6,8 +6,10 @@ package at.bitfire.davdroid.ui.setup import android.os.Bundle import android.view.Menu +import android.view.MenuInflater import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import at.bitfire.davdroid.App import at.bitfire.davdroid.R @@ -51,6 +53,22 @@ class LoginActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + addMenuProvider(object: MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.activity_login, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + if (menuItem.itemId == R.id.help) { + UiUtils.launchUri(this@LoginActivity, + App.homepageUrl(this@LoginActivity).buildUpon().appendPath("tested-with").build()) + return true + } + + return false + } + }) + if (savedInstanceState == null) { // first call, add first login fragment val factories = loginFragmentFactories // get factories from hilt @@ -71,14 +89,4 @@ class LoginActivity: AppCompatActivity() { } } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.activity_login, menu) - return true - } - - fun showHelp(item: MenuItem) { - UiUtils.launchUri(this, - App.homepageUrl(this).buildUpon().appendPath("tested-with").build()) - } - } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginModel.kt index 7aec2bfb2..976d30856 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginModel.kt @@ -7,6 +7,7 @@ package at.bitfire.davdroid.ui.setup import androidx.lifecycle.ViewModel import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.servicedetection.DavResourceFinder +import at.bitfire.vcard4android.GroupMethod import java.net.URI class LoginModel: ViewModel() { @@ -16,9 +17,10 @@ class LoginModel: ViewModel() { var configuration: DavResourceFinder.Configuration? = null - /** - * Account name that should be used as default account name when no email addresses have been found. - */ + /** account name that should be used as default account name when no email addresses have been found */ var suggestedAccountName: String? = null -} + /** group method that should be pre-selectedbr */ + var suggestedGroupMethod: GroupMethod = GroupMethod.GROUP_VCARDS + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginFlowFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginFlowFragment.kt index 423a4499a..4b2d3c721 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginFlowFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginFlowFragment.kt @@ -54,6 +54,7 @@ import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.ui.UiUtils.haveCustomTabs +import at.bitfire.vcard4android.GroupMethod import com.google.accompanist.themeadapter.material.MdcTheme import com.google.android.material.snackbar.Snackbar import dagger.Binds @@ -159,6 +160,7 @@ class NextcloudLoginFlowFragment: Fragment() { // continue to next fragment loginModel.baseURI = baseUri loginModel.credentials = credentials + loginModel.suggestedGroupMethod = GroupMethod.CATEGORIES parentFragmentManager.beginTransaction() .replace(android.R.id.content, DetectConfigurationFragment(), null) .addToBackStack(null) diff --git a/app/src/main/res/menu/activity_login.xml b/app/src/main/res/menu/activity_login.xml index e889350ce..0649926fe 100644 --- a/app/src/main/res/menu/activity_login.xml +++ b/app/src/main/res/menu/activity_login.xml @@ -7,8 +7,6 @@ android:id="@+id/help" android:title="@string/help" android:icon="@drawable/ic_help" - app:showAsAction="always" - android:onClick="showHelp"> - - + app:showAsAction="always" /> + \ No newline at end of file -- GitLab From e41ac428c9ba0e764934a3a36ca32765ca603106 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 17 Oct 2023 18:59:52 +0200 Subject: [PATCH 12/23] WebcalFragment: remove unused menu item code --- .../davdroid/ui/account/WebcalFragment.kt | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt index a3501070d..6c70a5143 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt @@ -50,7 +50,7 @@ class WebcalFragment: CollectionsFragment() { override val noCollectionsStringId = R.string.account_no_webcals @Inject lateinit var webcalModelFactory: WebcalModel.Factory - val webcalModel by viewModels() { + private val webcalModel by viewModels { object : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class) = @@ -69,21 +69,6 @@ class WebcalFragment: CollectionsFragment() { super.onPrepareMenu(menu) menu.findItem(R.id.create_calendar).isVisible = false } - - override fun onMenuItemSelected(menuItem: MenuItem): Boolean { - if (super.onMenuItemSelected(menuItem)) { - return true - } - - if (menuItem.itemId == R.id.create_calendar) { - val intent = Intent(requireActivity(), CreateCalendarActivity::class.java) - intent.putExtra(CreateCalendarActivity.EXTRA_ACCOUNT, accountModel.account) - startActivity(intent) - return true - } - - return false - } } override fun onCreate(savedInstanceState: Bundle?) { -- GitLab From c451c3fd70c8714c0055b17134eeff59561ac465 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 18 Oct 2023 15:08:52 +0200 Subject: [PATCH 13/23] Version bump to 4.3.9-beta.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 31ef4acaf..07421f39c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { defaultConfig { applicationId "at.bitfire.davdroid" - versionCode 403080000 - versionName '4.3.8' + versionCode 403090000 + versionName '4.3.9-beta.1' buildConfigField "long", "buildTime", System.currentTimeMillis() + "L" -- GitLab From f8330e8f52f71ee9a36a8ed9e5858176b883f8cd Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 18 Oct 2023 15:30:33 +0200 Subject: [PATCH 14/23] dnsjava: fix R8 rules --- app/proguard-rules-release.pro | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/proguard-rules-release.pro b/app/proguard-rules-release.pro index 9fac0cab2..eeaa9c7a1 100644 --- a/app/proguard-rules-release.pro +++ b/app/proguard-rules-release.pro @@ -30,9 +30,13 @@ # Additional rules which are now required since missing classes can't be ignored in R8 anymore. # [https://developer.android.com/build/releases/past-releases/agp-7-0-0-release-notes#r8-missing-class-warning] -dontwarn com.android.org.conscrypt.SSLParametersImpl +-dontwarn com.sun.jna.** # dnsjava -dontwarn groovy.** -dontwarn java.beans.Transient +-dontwarn javax.naming.NamingException # dnsjava +-dontwarn javax.naming.directory.** # dnsjava -dontwarn junit.textui.TestRunner +-dontwarn lombok.** # dnsjava -dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl -dontwarn org.bouncycastle.jsse.** -dontwarn org.codehaus.groovy.** -- GitLab From fe679da03b2d745993fa94d727edc406054a9375 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Thu, 19 Oct 2023 17:36:11 +0200 Subject: [PATCH 15/23] Refactor HiltViewModels ApplicationContext (#446) * Simplified ViewModels Signed-off-by: Arnau Mora * Fixed injection Signed-off-by: Arnau Mora * Fixed settings injection Signed-off-by: Arnau Mora * Added missing import Signed-off-by: Arnau Mora * Fixed application Signed-off-by: Arnau Mora * Fixed constructors and got rid of utils Signed-off-by: Arnau Mora * Optimized imports Signed-off-by: Arnau Mora * Added missing annotation Signed-off-by: Arnau Mora --------- Signed-off-by: Arnau Mora --- .../ui/webdav/AddWebdavMountActivityTest.kt | 4 ++-- .../at/bitfire/davdroid/ui/TasksFragment.kt | 10 ++++++---- .../ui/account/CreateCollectionFragment.kt | 15 +++++++-------- .../ui/account/DeleteCollectionFragment.kt | 11 +++++------ .../davdroid/ui/account/SettingsActivity.kt | 16 +++++++++++----- .../davdroid/ui/account/WebcalFragment.kt | 8 +++++--- .../davdroid/ui/setup/AccountDetailsFragment.kt | 10 ++++++---- .../davdroid/ui/webdav/AddWebdavMountActivity.kt | 10 ++++++---- .../davdroid/ui/webdav/WebdavMountsActivity.kt | 7 +++++-- 9 files changed, 53 insertions(+), 38 deletions(-) diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivityTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivityTest.kt index b62ff918c..edf26ad97 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivityTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivityTest.kt @@ -5,7 +5,7 @@ package at.bitfire.davdroid.ui.webdav import android.security.NetworkSecurityPolicy -import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.core.app.ApplicationProvider import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.WebDavMount import dagger.hilt.android.testing.HiltAndroidRule @@ -34,7 +34,7 @@ class AddWebdavMountActivityTest { fun setUp() { hiltRule.inject() - model = spyk(AddWebdavMountActivity.Model(InstrumentationRegistry.getInstrumentation().targetContext, db)) + model = spyk(AddWebdavMountActivity.Model(ApplicationProvider.getApplicationContext(), db)) Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksFragment.kt index 8e7242974..04dc578c6 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksFragment.kt @@ -4,6 +4,7 @@ package at.bitfire.davdroid.ui +import android.app.Application import android.content.Context import android.content.Intent import android.content.pm.PackageManager @@ -16,8 +17,8 @@ import androidx.annotation.AnyThread import androidx.databinding.ObservableBoolean import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.PackageChangedReceiver import at.bitfire.davdroid.R @@ -28,7 +29,6 @@ import at.bitfire.ical4android.TaskProvider.ProviderName import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject @AndroidEntryPoint @@ -102,9 +102,9 @@ class TasksFragment: Fragment() { @HiltViewModel class Model @Inject constructor( - @ApplicationContext val context: Context, + application: Application, val settings: SettingsManager - ) : ViewModel(), SettingsManager.OnChangeListener { + ) : AndroidViewModel(application), SettingsManager.OnChangeListener { companion object { @@ -117,6 +117,8 @@ class TasksFragment: Fragment() { } + val context: Context get() = getApplication() + val currentProvider = MutableLiveData() val openTasksInstalled = MutableLiveData() val openTasksRequested = MutableLiveData() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCollectionFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCollectionFragment.kt index 479c84c25..2a26f9715 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCollectionFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/CreateCollectionFragment.kt @@ -5,7 +5,7 @@ package at.bitfire.davdroid.ui.account import android.accounts.Account -import android.content.Context +import android.app.Application import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -15,20 +15,19 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.* import at.bitfire.dav4jvm.DavResource import at.bitfire.dav4jvm.XmlUtils -import at.bitfire.davdroid.util.DavUtils -import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.ui.ExceptionInfoFragment +import at.bitfire.davdroid.util.DavUtils import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.AndroidEntryPoint -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch @@ -117,12 +116,12 @@ class CreateCollectionFragment: DialogFragment() { class Model @AssistedInject constructor( - @ApplicationContext val context: Context, + application: Application, val db: AppDatabase, @Assisted val account: Account, @Assisted val serviceType: String, @Assisted val collection: Collection - ): ViewModel() { + ): AndroidViewModel(application) { @AssistedFactory interface Factory { @@ -133,7 +132,7 @@ class CreateCollectionFragment: DialogFragment() { fun createCollection(): LiveData { viewModelScope.launch(Dispatchers.IO + NonCancellable) { - HttpClient.Builder(context, AccountSettings(context, account)) + HttpClient.Builder(getApplication(), AccountSettings(getApplication(), account)) .setForeground(true) .build().use { httpClient -> try { @@ -148,7 +147,7 @@ class CreateCollectionFragment: DialogFragment() { db.collectionDao().insert(collection) // trigger service detection (because the collection may have other properties than the ones we have inserted) - RefreshCollectionsWorker.refreshCollections(context, service.id) + RefreshCollectionsWorker.refreshCollections(getApplication(), service.id) } // post success diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/DeleteCollectionFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/DeleteCollectionFragment.kt index 8101481da..c01b1f4dc 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/DeleteCollectionFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/DeleteCollectionFragment.kt @@ -5,7 +5,7 @@ package at.bitfire.davdroid.ui.account import android.accounts.Account -import android.content.Context +import android.app.Application import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -14,17 +14,16 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels import androidx.lifecycle.* import at.bitfire.dav4jvm.DavResource -import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.databinding.DeleteCollectionBinding import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.ui.ExceptionInfoFragment import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.AndroidEntryPoint -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch @@ -88,11 +87,11 @@ class DeleteCollectionFragment: DialogFragment() { class Model @AssistedInject constructor( - @ApplicationContext val context: Context, + application: Application, val db: AppDatabase, @Assisted var account: Account, @Assisted val collectionId: Long - ): ViewModel() { + ): AndroidViewModel(application) { @AssistedFactory interface Factory { @@ -114,7 +113,7 @@ class DeleteCollectionFragment: DialogFragment() { viewModelScope.launch(Dispatchers.IO + NonCancellable) { val collectionInfo = collectionInfo ?: return@launch - HttpClient.Builder(context, AccountSettings(context, account)) + HttpClient.Builder(getApplication(), AccountSettings(getApplication(), account)) .setForeground(true) .build().use { httpClient -> try { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/SettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/SettingsActivity.kt index a15c0ba68..dc9d28fb4 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/SettingsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/SettingsActivity.kt @@ -19,6 +19,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.app.TaskStackBuilder import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -425,10 +426,10 @@ class SettingsActivity: AppCompatActivity() { class Model @AssistedInject constructor( - val application: Application, + application: Application, val settings: SettingsManager, @Assisted val account: Account - ): ViewModel(), SettingsManager.OnChangeListener { + ): AndroidViewModel(application), SettingsManager.OnChangeListener { @AssistedFactory interface Factory { @@ -487,7 +488,9 @@ class SettingsActivity: AppCompatActivity() { fun reload() { val accountSettings = accountSettings ?: return - syncIntervalContacts.postValue(accountSettings.getSyncInterval(application.getString(R.string.address_books_authority))) + syncIntervalContacts.postValue( + accountSettings.getSyncInterval(getApplication().getString(R.string.address_books_authority)) + ) syncIntervalCalendars.postValue(accountSettings.getSyncInterval(CalendarContract.AUTHORITY)) syncIntervalTasks.postValue(tasksProvider?.let { accountSettings.getSyncInterval(it.authority) }) @@ -570,7 +573,10 @@ class SettingsActivity: AppCompatActivity() { accountSettings?.setGroupMethod(groupMethod) reload() - resync(application.getString(R.string.address_books_authority), fullResync = true) + resync( + authority = getApplication().getString(R.string.address_books_authority), + fullResync = true + ) } /** @@ -597,7 +603,7 @@ class SettingsActivity: AppCompatActivity() { */ private fun resync(authority: String, fullResync: Boolean) { val resync = if (fullResync) SyncWorker.FULL_RESYNC else SyncWorker.RESYNC - SyncWorker.enqueue(application, account, authority, resync) + SyncWorker.enqueue(getApplication(), account, authority, resync) } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt index 6c70a5143..50fb65350 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WebcalFragment.kt @@ -5,6 +5,7 @@ package at.bitfire.davdroid.ui.account import android.Manifest +import android.app.Application import android.content.ContentProviderClient import android.content.Context import android.content.Intent @@ -33,7 +34,6 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.AndroidEntryPoint -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import okhttp3.HttpUrl @@ -180,16 +180,18 @@ class WebcalFragment: CollectionsFragment() { class WebcalModel @AssistedInject constructor( - @ApplicationContext context: Context, + application: Application, val db: AppDatabase, @Assisted val serviceId: Long - ): ViewModel() { + ): AndroidViewModel(application) { @AssistedFactory interface Factory { fun create(serviceId: Long): WebcalModel } + val context: Context get() = getApplication() + private val resolver = context.contentResolver private var calendarPermission = false diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 2376be214..ac31eb309 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/kotlin/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.Application import android.content.ContentResolver import android.content.Context import android.content.Intent @@ -19,10 +20,10 @@ import android.widget.ArrayAdapter import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R @@ -44,7 +45,6 @@ import at.bitfire.vcard4android.GroupMethod import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch @@ -156,15 +156,17 @@ class AccountDetailsFragment : Fragment() { @HiltViewModel class Model @Inject constructor( - @ApplicationContext val context: Context, + application: Application, val db: AppDatabase, val settingsManager: SettingsManager - ) : ViewModel() { + ) : AndroidViewModel(application) { val name = MutableLiveData() val nameError = MutableLiveData() val showApostropheWarning = MutableLiveData(false) + val context: Context get() = getApplication() + fun validateAccountName(s: Editable) { showApostropheWarning.value = s.toString().contains('\'') nameError.value = null diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt index 3b5a9adb4..995f9b242 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt @@ -4,6 +4,7 @@ package at.bitfire.davdroid.ui.webdav +import android.app.Application import android.content.Context import android.os.Bundle import android.view.Menu @@ -14,8 +15,8 @@ import androidx.activity.viewModels import androidx.annotation.WorkerThread import androidx.appcompat.app.AppCompatActivity import androidx.core.view.MenuProvider +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.lifecycleScope import at.bitfire.dav4jvm.DavResource import at.bitfire.dav4jvm.UrlUtils @@ -33,7 +34,6 @@ import at.bitfire.davdroid.webdav.DavDocumentsProvider import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import okhttp3.HttpUrl @@ -156,9 +156,9 @@ class AddWebdavMountActivity: AppCompatActivity() { @HiltViewModel class Model @Inject constructor( - @ApplicationContext val context: Context, + application: Application, val db: AppDatabase - ) : ViewModel() { + ) : AndroidViewModel(application) { val displayName = MutableLiveData() val displayNameError = MutableLiveData() @@ -169,6 +169,8 @@ class AddWebdavMountActivity: AppCompatActivity() { val error = MutableLiveData() + val context: Context get() = getApplication() + @WorkerThread fun addMount(mount: WebDavMount, credentials: Credentials?): Boolean { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt index 6d3a0ae86..b17df1e68 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/webdav/WebdavMountsActivity.kt @@ -5,6 +5,7 @@ package at.bitfire.davdroid.ui.webdav import android.app.AlertDialog +import android.app.Application import android.content.Context import android.content.Intent import android.os.Bundle @@ -195,9 +196,11 @@ class WebdavMountsActivity: AppCompatActivity() { @HiltViewModel class Model @Inject constructor( - @ApplicationContext val context: Context, + application: Application, val db: AppDatabase - ): ViewModel() { + ): AndroidViewModel(application) { + + val context: Context get() = getApplication() val authority = context.getString(R.string.webdav_authority) -- GitLab From 8263b5fcf8f608826de4836ca1e4af891cf8768c Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 23 Oct 2023 10:41:01 +0200 Subject: [PATCH 16/23] WorkManager: add stop reason to debug info and sync logs (bitfireAT/davx5#413) --- .../main/kotlin/at/bitfire/davdroid/App.kt | 10 ++++---- .../RefreshCollectionsWorker.kt | 3 ++- .../davdroid/syncadapter/SyncWorker.kt | 23 +++---------------- .../bitfire/davdroid/ui/DebugInfoActivity.kt | 3 +-- .../davdroid/ui/account/AccountActivity.kt | 4 +--- build.gradle | 2 +- 6 files changed, 13 insertions(+), 32 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/App.kt b/app/src/main/kotlin/at/bitfire/davdroid/App.kt index d7c68f2cc..aca20baa3 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/App.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/App.kt @@ -63,6 +63,11 @@ class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provide @Inject lateinit var workerFactory: HiltWorkerFactory + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() + .setWorkerFactory(workerFactory) + .build() + override fun onCreate() { super.onCreate() Logger.initialize(this) @@ -108,11 +113,6 @@ class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provide } } - override fun getWorkManagerConfiguration() = - Configuration.Builder() - .setWorkerFactory(workerFactory) - .build() - override fun uncaughtException(t: Thread, e: Throwable) { Logger.log.log(Level.SEVERE, "Unhandled exception!", e) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt index ca8d36206..e1b48cea9 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt @@ -8,6 +8,7 @@ import android.accounts.Account import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.os.Build import androidx.concurrent.futures.CallbackToFutureAdapter import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -238,7 +239,7 @@ class RefreshCollectionsWorker @AssistedInject constructor( } override fun onStopped() { - Logger.log.info("Stopping refresh") + Logger.log.info("Stopping refresh (reason ${if (Build.VERSION.SDK_INT >= 31) stopReason else "n/a"})") refreshThread?.interrupt() } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncWorker.kt index be350e150..49608f304 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncWorker.kt @@ -5,11 +5,7 @@ package at.bitfire.davdroid.syncadapter import android.accounts.Account -import android.content.ContentProviderClient -import android.content.ContentResolver -import android.content.Context -import android.content.Intent -import android.content.SyncResult +import android.content.* import android.net.ConnectivityManager import android.net.wifi.WifiManager import android.os.Build @@ -23,20 +19,7 @@ import androidx.core.content.getSystemService import androidx.hilt.work.HiltWorker import androidx.lifecycle.LiveData import androidx.lifecycle.map -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.Data -import androidx.work.ExistingWorkPolicy -import androidx.work.ForegroundInfo -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.OutOfQuotaPolicy -import androidx.work.WorkInfo -import androidx.work.WorkManager -import androidx.work.WorkQuery -import androidx.work.WorkRequest -import androidx.work.Worker -import androidx.work.WorkerParameters +import androidx.work.* import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.network.ConnectionUtils.internetAvailable @@ -432,7 +415,7 @@ class SyncWorker @AssistedInject constructor( } override fun onStopped() { - Logger.log.info("Stopping sync thread") + Logger.log.info("Work stopped (reason ${if (Build.VERSION.SDK_INT >= 31) stopReason else "n/a"}), stopping sync thread") syncThread?.interrupt() } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt index 14f5cdfe8..5d80dd1a5 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt @@ -61,7 +61,6 @@ import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.exception.ExceptionUtils import org.dmfs.tasks.contract.TaskContract import java.io.* -import java.util.Locale import java.util.TimeZone import java.util.logging.Level import java.util.zip.ZipEntry @@ -680,7 +679,7 @@ class DebugInfoActivity : AppCompatActivity() { table.addLine( workInfo.tags.map { StringUtils.removeStartIgnoreCase(it, SyncWorker::class.java.getPackage()!!.name + ".") }, authority, - workInfo.state, + "${workInfo.state} (${workInfo.stopReason})", workInfo.runAttemptCount, workInfo.generation, workInfo.id diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt index 59adc5c56..18b1277e4 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt @@ -38,9 +38,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.util.logging.Level import javax.inject.Inject diff --git a/build.gradle b/build.gradle index b0b1646b7..2918c28c8 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { // * com.google.devtools.ksp at the end of this file okhttp: '4.11.0', room: '2.5.2', - workManager: '2.8.1', + workManager: '2.9.0-rc01', // Apache Commons versions commonsCollections: '4.4', commonsLang: '3.13.0', -- GitLab From 9dd82900043eee0f0f28cd7ba6587d45136e4f4a Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 24 Oct 2023 21:18:51 +0200 Subject: [PATCH 17/23] Debug info: show periodicity and next run of sync workers (bitfireAT/davx5#415) --- .../syncadapter/PeriodicSyncWorker.kt | 27 +++++++------------ .../bitfire/davdroid/ui/DebugInfoActivity.kt | 18 +++++++++---- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/PeriodicSyncWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/PeriodicSyncWorker.kt index 0d3533741..db211db84 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/PeriodicSyncWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/PeriodicSyncWorker.kt @@ -8,7 +8,15 @@ import android.accounts.Account import android.content.Context import android.provider.CalendarContract import androidx.hilt.work.HiltWorker -import androidx.work.* +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.Operation +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.settings.AccountSettings import dagger.assisted.Assisted @@ -95,23 +103,6 @@ class PeriodicSyncWorker @AssistedInject constructor( WorkManager.getInstance(context) .cancelUniqueWork(workerName(account, authority)) - /** - * Finds out whether the [PeriodicSyncWorker] is currently enqueued or running - * - * @param account account to check - * @param authority authority to check (for instance: [CalendarContract.AUTHORITY]]) - * @return boolean whether the [PeriodicSyncWorker] is running or enqueued - */ - fun isEnabled(context: Context, account: Account, authority: String): Boolean = - WorkManager.getInstance(context) - .getWorkInfos( - WorkQuery.Builder - .fromTags(listOf(workerName(account, authority))) - .addStates(listOf(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING)) - .build() - ).get() - .isNotEmpty() - } override fun doWork(): Result { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt index 5d80dd1a5..3fafc9f23 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt @@ -16,6 +16,7 @@ import android.net.Uri import android.os.* import android.provider.CalendarContract import android.provider.ContactsContract +import android.text.format.DateUtils import android.view.View import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -624,7 +625,7 @@ class DebugInfoActivity : AppCompatActivity() { } private fun dumpAccount(account: Account, infos: Iterable): String { - val table = TextTable("Authority", "getIsSyncable", "getSyncAutomatically", "PeriodicSyncWorker", "Interval", "Entries") + val table = TextTable("Authority", "isSyncable", "syncAutomatically", "Interval", "Entries") for (info in infos) { var nrEntries = "—" var client: ContentProviderClient? = null @@ -647,7 +648,6 @@ class DebugInfoActivity : AppCompatActivity() { info.authority, ContentResolver.getIsSyncable(account, info.authority), ContentResolver.getSyncAutomatically(account, info.authority), // content-triggered sync - PeriodicSyncWorker.isEnabled(context, account, info.authority), // should always be false for address book accounts accountSettings.getSyncInterval(info.authority)?.let {"${it/60} min"}, nrEntries ) @@ -661,7 +661,7 @@ class DebugInfoActivity : AppCompatActivity() { * whether they exist one by one */ private fun dumpSyncWorkersInfo(account: Account): String { - val table = TextTable("Tags", "Authority", "State", "Retries", "Generation", "ID") + val table = TextTable("Tags", "Authority", "State", "Next run", "Retries", "Generation", "Periodicity") listOf( context.getString(R.string.address_books_authority), CalendarContract.AUTHORITY, @@ -677,12 +677,20 @@ class DebugInfoActivity : AppCompatActivity() { WorkQuery.Builder.fromUniqueWorkNames(listOf(workerName)).build() ).get().forEach { workInfo -> table.addLine( - workInfo.tags.map { StringUtils.removeStartIgnoreCase(it, SyncWorker::class.java.getPackage()!!.name + ".") }, + workInfo.tags.map { it.replace("\\bat\\.bitfire\\.davdroid\\.".toRegex(), ".") }, authority, "${workInfo.state} (${workInfo.stopReason})", + workInfo.nextScheduleTimeMillis.let { nextRun -> + when (nextRun) { + Long.MAX_VALUE -> "—" + else -> DateUtils.getRelativeTimeSpanString(nextRun) + } + }, workInfo.runAttemptCount, workInfo.generation, - workInfo.id + workInfo.periodicityInfo?.let { periodicity -> + "every ${periodicity.repeatIntervalMillis/60000} min" + } ?: "not periodic" ) } } -- GitLab From 0215e98326ebcb0f2b972ba839aae26d127b166b Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 24 Oct 2023 21:21:28 +0200 Subject: [PATCH 18/23] Version bump to 4.3.9-beta.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 07421f39c..ffbc0540f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { defaultConfig { applicationId "at.bitfire.davdroid" - versionCode 403090000 - versionName '4.3.9-beta.1' + versionCode 403090001 + versionName '4.3.9-beta.2' buildConfigField "long", "buildTime", System.currentTimeMillis() + "L" -- GitLab From 1e6a457a0d2506b86afcc6acb13fbde198753fac Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Thu, 26 Oct 2023 11:33:53 +0200 Subject: [PATCH 19/23] Fix related google calendars not being found (bitfireAT/davx5#409) * Minor changes - update kdoc - rename method and variables * Add proxy parents to related resource detection * Rename argument, query ResourceType * Remove unnecessary utility method * Change parentOf to extension function; Always return URL with trailing slash * Use calendar-proxy-read/write ResourceType from new dav4jvm * Use max. two levels of recursion to detect shared Google calendars * Revise test and adapt method * Simplify HttpUrl.parent() --------- Co-authored-by: Ricki Hirner --- .../RefreshCollectionsWorkerTest.kt | 4 +- .../RefreshCollectionsWorker.kt | 102 ++++++++++-------- .../at/bitfire/davdroid/util/DavUtils.kt | 26 ++++- .../at/bitfire/davdroid/DavUtilsTest.kt | 20 +++- build.gradle | 4 +- 5 files changed, 107 insertions(+), 49 deletions(-) diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorkerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorkerTest.kt index 306649246..e885f32f4 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorkerTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorkerTest.kt @@ -143,13 +143,13 @@ class RefreshCollectionsWorkerTest { } @Test - fun testQueryHomesets() { + fun testDiscoverHomesets() { val service = createTestService(Service.TYPE_CARDDAV)!! val baseUrl = mockServer.url(PATH_CARDDAV + SUBPATH_PRINCIPAL) // Query home sets RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient) - .queryHomeSets(baseUrl) + .discoverHomesets(baseUrl) // Check home sets have been saved to database assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET/"), db.homeSetDao().getByService(service.id).first().url) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt index e1b48cea9..bbbd36059 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt @@ -63,6 +63,7 @@ import at.bitfire.davdroid.ui.DebugInfoActivity import at.bitfire.davdroid.ui.NotificationUtils import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible import at.bitfire.davdroid.ui.account.SettingsActivity +import at.bitfire.davdroid.util.DavUtils.parent import com.google.common.util.concurrent.ListenableFuture import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -192,7 +193,7 @@ class RefreshCollectionsWorker @AssistedInject constructor( // refresh home set list (from principal url) service.principal?.let { principalUrl -> Logger.log.fine("Querying principal $principalUrl for home sets") - refresher.queryHomeSets(principalUrl) + refresher.discoverHomesets(principalUrl) } // refresh home sets and their member collections @@ -280,28 +281,25 @@ class RefreshCollectionsWorker @AssistedInject constructor( val httpClient: OkHttpClient ) { + val alreadyQueried = mutableSetOf() + /** - * Checks if the given URL defines home sets and adds them to given home set list. - * - * @param principalUrl Principal URL to query - * @param forPersonalHomeset Whether this is the first call of this recursive method. - * Indicates that these found home sets are considered "personal", as they belong to the - * current-user-principal. + * Starting at current-user-principal URL, tries to recursively find and save all user relevant home sets. * - * Note: This is not be be confused with the DAV:owner attribute. Home sets can be owned by - * other principals and still be considered "personal" (belonging to the current-user-principal). * - * *true* = found home sets belong to the current-user-principal; recurse if - * calendar proxies or group memberships are found + * @param principalUrl URL of principal to query (user-provided principal or current-user-principal) + * @param level Current recursion level (limited to 0, 1 or 2): * - * *false* = found home sets don't directly belong to the current-user-principal; don't recurse + * - 0: We assume found home sets belong to the current-user-principal + * - 1 or 2: We assume found home sets don't directly belong to the current-user-principal * * @throws java.io.IOException * @throws HttpException * @throws at.bitfire.dav4jvm.exception.DavException */ - internal fun queryHomeSets(principalUrl: HttpUrl, forPersonalHomeset: Boolean = true) { - val related = mutableSetOf() + internal fun discoverHomesets(principalUrl: HttpUrl, level: Int = 0) { + Logger.log.fine("Discovering homesets of $principalUrl") + val relatedResources = mutableSetOf() // Define homeset class and properties to look for val homeSetClass: Class @@ -309,48 +307,62 @@ class RefreshCollectionsWorker @AssistedInject constructor( when (service.type) { Service.TYPE_CARDDAV -> { homeSetClass = AddressbookHomeSet::class.java - properties = arrayOf(DisplayName.NAME, AddressbookHomeSet.NAME, GroupMembership.NAME) + properties = arrayOf(DisplayName.NAME, AddressbookHomeSet.NAME, GroupMembership.NAME, ResourceType.NAME) } Service.TYPE_CALDAV -> { homeSetClass = CalendarHomeSet::class.java - properties = arrayOf(DisplayName.NAME, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME) + properties = arrayOf(DisplayName.NAME, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME, ResourceType.NAME) } else -> throw IllegalArgumentException() } - val dav = DavResource(httpClient, principalUrl) + // Query the URL + val principal = DavResource(httpClient, principalUrl) + val personal = level == 0 try { - // Query for the given service with properties - dav.propfind(0, *properties) { davResponse, _ -> - - // Check we got back the right service and save it - davResponse[homeSetClass]?.let { homeSet -> - for (href in homeSet.hrefs) - dav.location.resolve(href)?.let { - val foundUrl = UrlUtils.withTrailingSlash(it) + principal.propfind(0, *properties) { davResponse, _ -> + alreadyQueried += davResponse.href + + // If response holds home sets, save them + davResponse[homeSetClass]?.let { homeSets -> + for (homeSetHref in homeSets.hrefs) + principal.location.resolve(homeSetHref)?.let { homesetUrl -> + val resolvedHomeSetUrl = UrlUtils.withTrailingSlash(homesetUrl) + // Homeset is considered personal if this is the outer recursion call, + // This is because we assume the first call to query the current-user-principal + // Note: This is not be be confused with the DAV:owner attribute. Home sets can be owned by + // other principals and still be considered "personal" (belonging to the current-user-principal). db.homeSetDao().insertOrUpdateByUrl( - HomeSet(0, service.id, forPersonalHomeset, foundUrl) + HomeSet(0, service.id, personal, resolvedHomeSetUrl) ) } } - // If personal (outer call of recursion), find/refresh related resources - if (forPersonalHomeset) { - val relatedResourcesTypes = mapOf( - CalendarProxyReadFor::class.java to "read-only proxy for", // calendar-proxy-read-for - CalendarProxyWriteFor::class.java to "read/write proxy for ", // calendar-proxy-read/write-for - GroupMembership::class.java to "member of group") // direct group memberships - - for ((type, logString) in relatedResourcesTypes) { + // Add related principals to be queried afterwards + if (personal) { + val relatedResourcesTypes = listOf( + // current resource is a read/write-proxy for other principals + CalendarProxyReadFor::class.java, + CalendarProxyWriteFor::class.java, + // current resource is a member of a group (principal that can also have proxies) + GroupMembership::class.java) + for (type in relatedResourcesTypes) davResponse[type]?.let { - for (href in it.hrefs) { - Logger.log.fine("Principal is a $logString for $href, checking for home sets") - dav.location.resolve(href)?.let { url -> - related += url + for (href in it.hrefs) + principal.location.resolve(href)?.let { url -> + relatedResources += url } - } } - } + } + + // If current resource is a calendar-proxy-read/write, it's likely that its parent is a principal, too. + davResponse[ResourceType::class.java]?.let { resourceType -> + val proxyProperties = arrayOf( + ResourceType.CALENDAR_PROXY_READ, + ResourceType.CALENDAR_PROXY_WRITE, + ) + if (proxyProperties.any { resourceType.types.contains(it) }) + relatedResources += davResponse.href.parent() } } } catch (e: HttpException) { @@ -360,9 +372,13 @@ class RefreshCollectionsWorker @AssistedInject constructor( throw e } - // query related homesets (those that do not belong to the current-user-principal) - for (resource in related) - queryHomeSets(resource, false) + // query related resources + if (level <= 1) + for (resource in relatedResources) + if (alreadyQueried.contains(resource)) + Logger.log.warning("$resource already queried, skipping") + else + discoverHomesets(resource, level + 1) } /** diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/DavUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/DavUtils.kt index d0d3f2caf..136087245 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/DavUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/DavUtils.kt @@ -8,8 +8,8 @@ import android.content.Context import android.net.ConnectivityManager import android.os.Build import androidx.core.content.getSystemService -import at.bitfire.davdroid.network.Android10Resolver import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.network.Android10Resolver import okhttp3.HttpUrl import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType @@ -151,6 +151,30 @@ object DavUtils { // extension methods + /** + * Returns parent URL (parent folder). Always with trailing slash + */ + fun HttpUrl.parent(): HttpUrl { + if (pathSegments.size == 1 && pathSegments[0] == "") + // already root URL + return this + + val builder = newBuilder() + + if (pathSegments[pathSegments.lastIndex] == "") { + // URL ends with a slash ("/some/thing/" -> ["some","thing",""]), remove two segments ("" at lastIndex and "thing" at lastIndex - 1) + builder.removePathSegment(pathSegments.lastIndex) + builder.removePathSegment(pathSegments.lastIndex - 1) + } else + // URL doesn't end with a slash ("/some/thing" -> ["some","thing"]), remove one segment ("thing" at lastIndex) + builder.removePathSegment(pathSegments.lastIndex) + + // append trailing slash + builder.addPathSegment("") + + return builder.build() + } + /** * Compares MIME type and subtype of two MediaTypes. Does _not_ compare parameters * like `charset` or `version`. diff --git a/app/src/test/kotlin/at/bitfire/davdroid/DavUtilsTest.kt b/app/src/test/kotlin/at/bitfire/davdroid/DavUtilsTest.kt index 0a64cab91..fada87538 100644 --- a/app/src/test/kotlin/at/bitfire/davdroid/DavUtilsTest.kt +++ b/app/src/test/kotlin/at/bitfire/davdroid/DavUtilsTest.kt @@ -5,8 +5,11 @@ package at.bitfire.davdroid import at.bitfire.davdroid.util.DavUtils +import at.bitfire.davdroid.util.DavUtils.parent import okhttp3.HttpUrl.Companion.toHttpUrl -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Test import org.xbill.DNS.DClass import org.xbill.DNS.Name @@ -31,6 +34,21 @@ class DavUtilsTest { assertEquals("file.html", DavUtils.lastSegmentOfUrl((exampleURL + "dir/file.html").toHttpUrl())) } + @Test + fun testParent() { + // with trailing slash + assertEquals("http://example.com/1/2/".toHttpUrl(), "http://example.com/1/2/3/".toHttpUrl().parent()) + assertEquals("http://example.com/1/".toHttpUrl(), "http://example.com/1/2/".toHttpUrl().parent()) + assertEquals("http://example.com/".toHttpUrl(), "http://example.com/1/".toHttpUrl().parent()) + assertEquals("http://example.com/".toHttpUrl(), "http://example.com/".toHttpUrl().parent()) + + // without trailing slash + assertEquals("http://example.com/1/2/".toHttpUrl(), "http://example.com/1/2/3".toHttpUrl().parent()) + assertEquals("http://example.com/1/".toHttpUrl(), "http://example.com/1/2".toHttpUrl().parent()) + assertEquals("http://example.com/".toHttpUrl(), "http://example.com/1".toHttpUrl().parent()) + assertEquals("http://example.com/".toHttpUrl(), "http://example.com".toHttpUrl().parent()) + } + @Test fun testSelectSRVRecord() { assertNull(DavUtils.selectSRVRecord(emptyArray())) diff --git a/build.gradle b/build.gradle index 2918c28c8..090ea7295 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { hilt: '2.48.1', kotlin: '1.9.10', // keep in sync with * app/build.gradle composeOptions.kotlinCompilerExtensionVersion // * com.google.devtools.ksp at the end of this file - okhttp: '4.11.0', + okhttp: '4.12.0', room: '2.5.2', workManager: '2.9.0-rc01', // Apache Commons versions @@ -19,7 +19,7 @@ buildscript { commonsText: '1.10.0', // own libraries cert4android: '2bb3898', - dav4jvm: 'da94a8b', + dav4jvm: '1ed89c1', ical4android: '916f222', vcard4android: 'b376d2e' ] -- GitLab From 73475640f794996916c884218ef1a4a02eff1f48 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 26 Oct 2023 11:58:05 +0200 Subject: [PATCH 20/23] Google Login: minor UI improvements (bitfireAT/davx5#416) - automatically append @gmail.com - show Go IME action for login and client ID --- .../davdroid/ui/setup/GoogleLoginFragment.kt | 71 +++++++++++++------ 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/GoogleLoginFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/GoogleLoginFragment.kt index 8ebe15854..6ef6e0971 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/GoogleLoginFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/GoogleLoginFragment.kt @@ -17,16 +17,26 @@ import android.view.ViewGroup import android.widget.TextView import androidx.activity.result.contract.ActivityResultContract import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -34,6 +44,7 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.tooling.preview.Preview @@ -58,7 +69,14 @@ import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import net.openid.appauth.* +import net.openid.appauth.AuthState +import net.openid.appauth.AuthorizationException +import net.openid.appauth.AuthorizationRequest +import net.openid.appauth.AuthorizationResponse +import net.openid.appauth.AuthorizationService +import net.openid.appauth.AuthorizationServiceConfiguration +import net.openid.appauth.ResponseTypeValues +import net.openid.appauth.TokenResponse import org.apache.commons.lang3.StringUtils import java.net.URI import java.util.logging.Level @@ -236,31 +254,52 @@ fun GoogleLogin( } val email = rememberSaveable { mutableStateOf(defaultEmail ?: "") } - val emailError = remember { mutableStateOf(false) } + val userClientId = rememberSaveable { mutableStateOf("") } + + fun login() { + // append @gmail.com, if necessary + val loginEmail = email.value.let { + if (it.contains('@')) + it + else + it + "@gmail.com" + } + + val clientId = StringUtils.trimToNull(userClientId.value.trim()) + onLogin(loginEmail, clientId) + } OutlinedTextField( email.value, singleLine = true, onValueChange = { email.value = it }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Go + ), + keyboardActions = KeyboardActions( + onGo = { login() } + ), label = { Text(stringResource(R.string.login_google_account)) }, - isError = emailError.value, placeholder = { Text("example@gmail.com") }, modifier = Modifier .fillMaxWidth() .padding(top = 8.dp) ) - val userClientId = rememberSaveable { mutableStateOf("") } - val userClientIdError = remember { mutableStateOf(false) } OutlinedTextField( userClientId.value, singleLine = true, onValueChange = { clientId -> userClientId.value = clientId }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Go + ), + keyboardActions = KeyboardActions( + onGo = { login() } + ), label = { Text(stringResource(R.string.login_google_client_id)) }, - isError = userClientIdError.value, placeholder = { Text("[...].apps.googleusercontent.com") }, modifier = Modifier .fillMaxWidth() @@ -268,15 +307,7 @@ fun GoogleLogin( ) Button( - onClick = { - val validEmail = email.value.contains('@') - emailError.value = !validEmail - - if (validEmail) { - val clientId = StringUtils.trimToNull(userClientId.value.trim()) - onLogin(email.value, clientId) - } - }, + onClick = { login() }, modifier = Modifier .padding(top = 8.dp) .wrapContentSize(), -- GitLab From 046bacff3f843706759c407f561bb2003393312a Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 26 Oct 2023 12:14:43 +0200 Subject: [PATCH 21/23] Version bump to 4.3.9 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ffbc0540f..1b9d7e867 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { defaultConfig { applicationId "at.bitfire.davdroid" - versionCode 403090001 - versionName '4.3.9-beta.2' + versionCode 403090002 + versionName '4.3.9' buildConfigField "long", "buildTime", System.currentTimeMillis() + "L" -- GitLab From 1a36ee2d60d59362f5f9da72342dae7d0bc715cb Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 26 Oct 2023 13:15:04 +0200 Subject: [PATCH 22/23] Fetch translations from Transifex --- app/src/main/res/values-de/strings.xml | 15 ++++++++++++++- app/src/main/res/values-fr/strings.xml | 11 +++++++++++ app/src/main/res/values-gl/strings.xml | 8 ++++++++ app/src/main/res/values-ja/strings.xml | 8 ++++++++ app/src/main/res/values-ro/strings.xml | 5 +++++ app/src/main/res/values-ru/strings.xml | 16 ++++++++++++---- app/src/main/res/values-zh/strings.xml | 8 ++++++++ 7 files changed, 66 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index af277d089..4f423bf5b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -128,7 +128,9 @@ Spenden Datenschutzerklärung Benachrichtigungen deaktiviert. Sie werden nicht über Fehler bei der Synchronisation informiert. + Keine überprüfte Internetverbindung. Synchronisierung funktioniert eventuell nicht. Verbindungen steuern + Wenig Speicherplatz. Android wird lokale Änderungen nicht sofort synchronisieren, sondern bei der nächsten regulären Synchronisierung. Speicherplatz verwalten Datensparen aktiviert. Die Hintergrundsynchronisierung ist eingeschränkt. Datensparen verwalten @@ -265,6 +267,14 @@ Datenschutzrichtliniefür mehr Informationen.]]> Google API Services Nutzerdaten-Richtlinie, inklusive der eingeschränkten Nutzungsbedingungen.]]> Authentifizierungscode konnte nicht abgerufen werden + Nextcloud + Mit Nextcloud anmelden + Anmeldevorgang + Dadurch wird der Nextcloud-Anmeldevorgang in einem Webbrowser gestartet. + Nextcloud-Serveradresse + Anmeldung + Login-URL konnte nicht abgerufen werden + Anmeldedaten konnten nicht abgerufen werden Ressourcen-Erkennung Server-Informationen werden abgerufen. Bitte warten … Es konnte weder ein CalDAV- noch ein CardDAV-Dienst gefunden werden. @@ -297,8 +307,11 @@ Erlaubte WLAN-Namen (SSIDs, mit Komma getrennt, leer lassen für alle) WLAN-SSID-Einschränkung benötigt weitere Einstellungen Verwalten + VPN erfordert zugrundeliegendes Internet + VPN ohne zugrundeliegende überprüfte Internetverbindung reicht für Synchronisierung nicht aus (empfohlen) + VPN ohne zugrundeliegende überprüfte Internetverbindung reicht für Synchronisierung aus Anmeldeinformationen - Wieder anmelden + Erneut anmelden Erneut mit OAuth anmelden Benutzername Benutzername diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 5c1b86d54..baf3b7001 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -127,7 +127,9 @@ Faire un don Politique de confidentialité Notifications désactivées. Vous ne serez pas averti des erreurs de synchronisation. + Pas de connexion Internet. La synchronisation risque de ne pas fonctionner. Gérer les connexions + Espace de stockage faible. Android ne synchronisera pas les changements locaux immédiatement mais pendant la prochaine synchronisation. Gérer le stockage L\'économiseur de données est activé. La synchronisation en arrière-plan est limitée. Gérer l\'économiseur de données @@ -255,6 +257,12 @@ Utiliser le certificat du client Aucun certificat trouvé Installer un certificat + Google Contacts / Agenda + Merci de vous référer à la page \"Testé avec Google\" pour des informations à jour. + Vous risquez de rencontrer des avertissements inattendus et/ou vous devez créer votre propre ID client. + Compte Google + Se connecter avec Google + N\'a pas pu obtenir le code d\'autorisation Détection de la configuration Veuillez patienter, nous interrogeons le serveur … Aucun accès possible au service CalDAV ou CardDAV. @@ -288,6 +296,7 @@ La restriction du SSID WiFi nécessite des réglages supplémentaires Gérer Authentification + nom d\'utilisateur Nom d\'utilisateur Saisissez votre nom d\'utilisateur : Mot de passe @@ -347,6 +356,8 @@ Ces données sont supprimées du serveur. Forcer la lecture seulement Propriétés + Dernièrement synchronisé : + Jamais synchronisé Adresse (URL) : Propriétaire : diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 38b621951..398c5c52f 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -267,6 +267,14 @@ política de Privacidade para saber máis.]]> Google API Services User Data Policy, incluíndo os requerimentos de Limited Use.]]> Non se puido obter o código de autorización + Nextcloud + Acceder con Nextcloud + Acceso Asistido + Accederás usando Nextcloud nun navegador Web. + Enderezo do servidor Nextcloud + Acceder + Non se obtivo o URL de acceso + Non se obtiveron os datos de acceso Detección da configuración Agarda por favor, consultando o servidor… Non se atopou servizo CalDAV ou CardDAV. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 9b09c11d5..d7881acc2 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -267,6 +267,14 @@ プライバシーポリシー をご確認ください。]]> Google API サービスのユーザーデータに関するポリシー に準拠しています。]]> 認証コードを取得できませんでした + Nextcloud + Nextcloud でログイン + ログインフロー + ウェブブラウザーでのログインフローが開始されます。 + Nextcloud サーバーアドレス + サインイン + ログイン URL を入手できませんでした + ログイン情報を入手できませんでした 設定の検出 しばらくお待ちください。サーバーに問い合わせ中… CalDAV または CardDAV サービスが見つかりませんでした。 diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 64827abb6..758ae3a44 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -128,7 +128,9 @@ Donează Politica de confidențialitate Notificări dezactivate. Nu vei fi notificat despre erorile de sincronizare. + Nicio conexiune la internet validată. Este posibil ca sincronizarea să nu ruleze. Gestionează conexiunile + Spațiu de depozitare redus. Android nu va sincroniza modificările locale imediat, ci în timpul următoarei sincronizări obișnuite. Gestionează stocarea Economizorul de date este activat. Sincronizarea în fundal este restricționată. Gestionează economizorul de date @@ -297,6 +299,9 @@ Nume separate prin virgulă (SSID) ale rețelelor WiFi permise (lasă necompletat pentru toate) Restricția SSID WiFi necesită setări suplimentare Gestionează + VPN necesită internetul de bază + VPN fără conexiune validată la Internet nu este suficient pentru a rula sincronizarea (recomandat) + VPN fără conexiune validată la Internet este suficient pentru a rula sincronizarea Autentificare Reautentificare Efectuează din nou autentificarea OAuth diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b1756fc12..0d2197dda 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -159,7 +159,7 @@ Приложение добавлено в белый список (рекомендуется) Приложение не добавлено в белый список (не рекомендуется) Приоритетный режим - Может помочь, если ваше устройство препятствует автоматической синхронизации + Может помочь, если устройство препятствует автоматической синхронизации Соединение Тип прокси @@ -267,6 +267,14 @@ Политику конфиденциальности.]]> Политику в отношении пользовательских данных Google API Services, включая требования Ограниченного использования.]]> Не удалось получить код авторизации + Nextcloud + Войти через Nextcloud + Процесс авторизации + Это запустит процесс авторизации в Nextcloud в браузере. + Адрес сервера Nextcloud + Войти + Не удалось получить URL для авторизации + Не удалось получить данные для авторизации Обнаружение конфигурации Ожидайте, выполняется запрос к серверу… Не удалось найти службу CalDAV или CardDAV. @@ -299,9 +307,9 @@ Имена (SSID) разрешенных сетей WiFi, разделенные запятыми (оставьте пустым для всех) Ограничение WiFi SSID требует дополнительных настроек Управлять - Для работы VPN требуется основной интернет - VPN без проверенного подключения к интернету недостаточно для синхронизации (рекомендуется) - Для синхронизации достаточно VPN без проверенного подключения к интернету. + VPN требует наличия основного интернета + VPN без основного интернета недостаточно для выполнения синхронизации (рекомендуется) + VPN без основного интернета достаточно для выполнения синхронизации Аутентификация Повторная аутентификация Выполните вход через OAuth повторно diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index c99cabc72..7a403d074 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -267,6 +267,14 @@ 隐私政策 。]]> Google API 服务用户数据政策,包括有限使用的要求。]]> 无法获得身份验证码 + Nextcloud + 用 Nextcloud 登录 + 登录流程 + 这会在网页浏览器中开启 Nextcloud 登录流程 + Nextcloud 服务器地址 + 登录 + 无法获取登录 URL + 无法获得登陆数据 正在配置 正在与服务器通信,请稍等… 找不到 CalDAV 或 CardDAV 服务。 -- GitLab From 2522378a3c482904120a1c982290b76a5686e883 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Wed, 1 Nov 2023 19:33:29 +0600 Subject: [PATCH 23/23] fix icon color mismatch issue --- .../main/res/drawable/ic_account_circle_white.xml | 12 ++++++++---- app/src/main/res/drawable/ic_cloud_off.xml | 2 +- app/src/main/res/drawable/ic_datasaver_on.xml | 14 ++++++++++---- app/src/main/res/drawable/ic_done.xml | 2 +- app/src/main/res/drawable/ic_error.xml | 2 +- .../res/drawable/ic_folder_refresh_outline.xml | 11 ++++++++++- app/src/main/res/drawable/ic_notifications.xml | 2 +- app/src/main/res/drawable/ic_notifications_off.xml | 13 +++++++++---- app/src/main/res/drawable/ic_remove.xml | 14 ++++++++++---- app/src/main/res/drawable/ic_sd_card_notify.xml | 2 +- app/src/main/res/drawable/ic_storage.xml | 12 ++++++++---- app/src/main/res/drawable/ic_sync_action.xml | 12 ++++++++---- .../main/res/drawable/ic_sync_problem_notify.xml | 2 +- app/src/main/res/drawable/ic_sync_shortcut.xml | 2 +- app/src/main/res/drawable/ic_warning_notify.xml | 2 +- .../main/res/drawable/intro_logo_background.xml | 11 ++++++++--- app/src/main/res/layout/activity_account.xml | 1 - 17 files changed, 79 insertions(+), 37 deletions(-) diff --git a/app/src/main/res/drawable/ic_account_circle_white.xml b/app/src/main/res/drawable/ic_account_circle_white.xml index 77b0bb954..653c07eaa 100644 --- a/app/src/main/res/drawable/ic_account_circle_white.xml +++ b/app/src/main/res/drawable/ic_account_circle_white.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_cloud_off.xml b/app/src/main/res/drawable/ic_cloud_off.xml index e795748c5..ae499bcd2 100644 --- a/app/src/main/res/drawable/ic_cloud_off.xml +++ b/app/src/main/res/drawable/ic_cloud_off.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_datasaver_on.xml b/app/src/main/res/drawable/ic_datasaver_on.xml index 1b55f9611..59f500cc8 100644 --- a/app/src/main/res/drawable/ic_datasaver_on.xml +++ b/app/src/main/res/drawable/ic_datasaver_on.xml @@ -1,5 +1,11 @@ - - + + diff --git a/app/src/main/res/drawable/ic_done.xml b/app/src/main/res/drawable/ic_done.xml index 07f2e7d2b..993f0249b 100644 --- a/app/src/main/res/drawable/ic_done.xml +++ b/app/src/main/res/drawable/ic_done.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_error.xml b/app/src/main/res/drawable/ic_error.xml index a375cc26b..4712db098 100644 --- a/app/src/main/res/drawable/ic_error.xml +++ b/app/src/main/res/drawable/ic_error.xml @@ -12,6 +12,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_folder_refresh_outline.xml b/app/src/main/res/drawable/ic_folder_refresh_outline.xml index b72d56709..061061442 100644 --- a/app/src/main/res/drawable/ic_folder_refresh_outline.xml +++ b/app/src/main/res/drawable/ic_folder_refresh_outline.xml @@ -1 +1,10 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notifications.xml b/app/src/main/res/drawable/ic_notifications.xml index 1bdb070b0..cac29078b 100644 --- a/app/src/main/res/drawable/ic_notifications.xml +++ b/app/src/main/res/drawable/ic_notifications.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_notifications_off.xml b/app/src/main/res/drawable/ic_notifications_off.xml index daa24d19b..dc5cdff5c 100644 --- a/app/src/main/res/drawable/ic_notifications_off.xml +++ b/app/src/main/res/drawable/ic_notifications_off.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_remove.xml b/app/src/main/res/drawable/ic_remove.xml index 8561a3390..868b390be 100644 --- a/app/src/main/res/drawable/ic_remove.xml +++ b/app/src/main/res/drawable/ic_remove.xml @@ -1,5 +1,11 @@ - - + + diff --git a/app/src/main/res/drawable/ic_sd_card_notify.xml b/app/src/main/res/drawable/ic_sd_card_notify.xml index 5e55c985d..c0481aef5 100644 --- a/app/src/main/res/drawable/ic_sd_card_notify.xml +++ b/app/src/main/res/drawable/ic_sd_card_notify.xml @@ -6,7 +6,7 @@ diff --git a/app/src/main/res/drawable/ic_storage.xml b/app/src/main/res/drawable/ic_storage.xml index 9e28a8a14..e8fd64937 100644 --- a/app/src/main/res/drawable/ic_storage.xml +++ b/app/src/main/res/drawable/ic_storage.xml @@ -1,6 +1,10 @@ - - + android:viewportWidth="24" + android:viewportHeight="24"> + diff --git a/app/src/main/res/drawable/ic_sync_action.xml b/app/src/main/res/drawable/ic_sync_action.xml index 6b9cd3cf2..5c2c18643 100644 --- a/app/src/main/res/drawable/ic_sync_action.xml +++ b/app/src/main/res/drawable/ic_sync_action.xml @@ -1,6 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_sync_problem_notify.xml b/app/src/main/res/drawable/ic_sync_problem_notify.xml index d3c1906ef..2af03580a 100644 --- a/app/src/main/res/drawable/ic_sync_problem_notify.xml +++ b/app/src/main/res/drawable/ic_sync_problem_notify.xml @@ -6,7 +6,7 @@ diff --git a/app/src/main/res/drawable/ic_sync_shortcut.xml b/app/src/main/res/drawable/ic_sync_shortcut.xml index 687320546..ab8d98c0a 100644 --- a/app/src/main/res/drawable/ic_sync_shortcut.xml +++ b/app/src/main/res/drawable/ic_sync_shortcut.xml @@ -4,6 +4,6 @@ android:viewportHeight="24.0" android:viewportWidth="24.0"> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_warning_notify.xml b/app/src/main/res/drawable/ic_warning_notify.xml index f6ee92b30..0b592e5e5 100644 --- a/app/src/main/res/drawable/ic_warning_notify.xml +++ b/app/src/main/res/drawable/ic_warning_notify.xml @@ -6,7 +6,7 @@ diff --git a/app/src/main/res/drawable/intro_logo_background.xml b/app/src/main/res/drawable/intro_logo_background.xml index b84f013b9..882b1331a 100644 --- a/app/src/main/res/drawable/intro_logo_background.xml +++ b/app/src/main/res/drawable/intro_logo_background.xml @@ -1,6 +1,11 @@ - - + + diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index 8845b6c7e..b34925fd2 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -46,7 +46,6 @@ android:layout_height="wrap_content" android:layout_gravity="top|end" android:layout_margin="@dimen/fab_margin" - app:backgroundTint="@android:color/white" app:tint="@color/grey900" app:srcCompat="@drawable/ic_folder_refresh_outline" app:layout_anchor="@id/sync" -- GitLab