mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-23 19:50:18 +00:00
Add last synced time to collection properties (bitfireAT/davx5#260)
* Add last synced time to collection properties * Show last synced time for every to the collection relevant authority * Try finding the application name for given package name * Resolve authority to package name before finding application label * Use AndroidViewModel instead of ViewModel; change model LiveData to val * Rewrite to Compose, use relative time description --------- Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
parent
5aff37a9a6
commit
a072cf0404
|
@ -111,7 +111,7 @@ dependencies {
|
|||
implementation project(':vcard4android')
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1"
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
|
||||
implementation "com.google.dagger:hilt-android:${versions.hilt}"
|
||||
|
@ -122,7 +122,7 @@ dependencies {
|
|||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.core:core-ktx:1.10.0'
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.7'
|
||||
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
||||
|
@ -141,6 +141,7 @@ dependencies {
|
|||
implementation composeBom
|
||||
androidTestImplementation composeBom
|
||||
implementation 'androidx.compose.material:material'
|
||||
implementation 'androidx.compose.runtime:runtime-livedata'
|
||||
implementation 'com.google.accompanist:accompanist-themeadapter-material:0.30.1'
|
||||
|
||||
// Jetpack Room
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface SyncStatsDao {
|
||||
|
@ -14,4 +16,6 @@ interface SyncStatsDao {
|
|||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertOrReplace(syncStats: SyncStats)
|
||||
|
||||
@Query("SELECT * FROM syncstats WHERE collectionId=:id")
|
||||
fun getLiveByCollectionId(id: Long): LiveData<List<SyncStats>>
|
||||
}
|
|
@ -4,17 +4,39 @@
|
|||
|
||||
package at.bitfire.davdroid.ui.account
|
||||
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.ContactsContract
|
||||
import android.text.format.DateUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.switchMap
|
||||
import at.bitfire.davdroid.databinding.CollectionPropertiesBinding
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.TaskUtils
|
||||
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
@ -39,7 +61,7 @@ class CollectionInfoFragment: DialogFragment() {
|
|||
}
|
||||
|
||||
@Inject lateinit var modelFactory: Model.Factory
|
||||
val model by viewModels<Model>() {
|
||||
val model by viewModels<Model> {
|
||||
object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>) =
|
||||
|
@ -48,31 +70,110 @@ class CollectionInfoFragment: DialogFragment() {
|
|||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val view = CollectionPropertiesBinding.inflate(inflater, container, false)
|
||||
view.lifecycleOwner = this
|
||||
view.model = model
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
MdcTheme {
|
||||
CollectionInfoDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return view.root
|
||||
@Composable
|
||||
fun CollectionInfoDialog() {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
// URL
|
||||
val collectionState = model.collection.observeAsState()
|
||||
collectionState.value?.let { collection ->
|
||||
Text(stringResource(R.string.collection_properties_url), style = MaterialTheme.typography.h5)
|
||||
Text(collection.url.toString(), modifier = Modifier.padding(bottom = 16.dp), fontFamily = FontFamily.Monospace)
|
||||
}
|
||||
|
||||
// Owner
|
||||
val owner = model.owner.observeAsState()
|
||||
owner.value?.let { principal ->
|
||||
Text(stringResource(R.string.collection_properties_owner), style = MaterialTheme.typography.h5)
|
||||
Text(principal.displayName ?: principal.url.toString(), Modifier.padding(bottom = 16.dp))
|
||||
}
|
||||
|
||||
// Last synced (for all applicable authorities)
|
||||
val lastSyncedState = model.lastSynced.observeAsState()
|
||||
lastSyncedState.value?.let { lastSynced ->
|
||||
Text(stringResource(R.string.collection_properties_sync_time), style = MaterialTheme.typography.h5)
|
||||
if (lastSynced.isEmpty())
|
||||
Text(stringResource(R.string.collection_properties_sync_time_never))
|
||||
else
|
||||
for ((app, timestamp) in lastSynced.entries) {
|
||||
Text(app)
|
||||
val timeStr = DateUtils.getRelativeDateTimeString(requireContext(), timestamp,
|
||||
DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0).toString()
|
||||
Text(timeStr, Modifier.padding(bottom = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Model @AssistedInject constructor(
|
||||
application: Application,
|
||||
val db: AppDatabase,
|
||||
@Assisted collectionId: Long
|
||||
): ViewModel() {
|
||||
): AndroidViewModel(application) {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(collectionId: Long): Model
|
||||
}
|
||||
|
||||
var collection = db.collectionDao().getLive(collectionId)
|
||||
var owner = collection.switchMap { collection ->
|
||||
val collection = db.collectionDao().getLive(collectionId)
|
||||
val owner = collection.switchMap { collection ->
|
||||
collection.ownerId?.let { ownerId ->
|
||||
db.principalDao().getLive(ownerId)
|
||||
}
|
||||
}
|
||||
|
||||
val lastSynced: LiveData<Map<String, Long>> = // map: app name -> last sync timestamp
|
||||
db.syncStatsDao().getLiveByCollectionId(collectionId).map { syncStatsList ->
|
||||
// map: authority -> syncStats
|
||||
val syncStatsMap = syncStatsList.associateBy { it.authority }
|
||||
|
||||
val interestingAuthorities = listOfNotNull(
|
||||
ContactsContract.AUTHORITY,
|
||||
CalendarContract.AUTHORITY,
|
||||
TaskUtils.currentProvider(getApplication())?.authority
|
||||
)
|
||||
|
||||
val result = mutableMapOf<String, Long>()
|
||||
// map (authority name) -> (app name, last sync timestamp)
|
||||
for (authority in interestingAuthorities) {
|
||||
val lastSync = syncStatsMap[authority]?.lastSync
|
||||
if (lastSync != null)
|
||||
result[getAppNameFromAuthority(authority)] = lastSync
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the application name for given authority. Returns the authority if not
|
||||
* found.
|
||||
*
|
||||
* @param authority authority to find the application name for (ie "at.techbee.jtx")
|
||||
* @return the application name of authority (ie "jtx Board")
|
||||
*/
|
||||
private fun getAppNameFromAuthority(authority: String): String {
|
||||
val packageManager = getApplication<Application>().packageManager
|
||||
@Suppress("DEPRECATION")
|
||||
val packageName = packageManager.resolveContentProvider(authority, 0)?.packageName ?: authority
|
||||
return try {
|
||||
@Suppress("DEPRECATION")
|
||||
val appInfo = packageManager.getPackageInfo(packageName, 0).applicationInfo
|
||||
packageManager.getApplicationLabel(appInfo).toString()
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Logger.log.warning("Application name not found for authority: $authority")
|
||||
authority
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="model"
|
||||
type="at.bitfire.davdroid.ui.account.CollectionInfoFragment.Model"/>
|
||||
<import type="android.view.View"/>
|
||||
</data>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.MaterialComponents.Headline5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{model.collection.title()}"
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="Collection Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/url_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/collection_properties_url"
|
||||
android:labelFor="@id/url" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/url"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:text="@{model.collection.url.toString()}"
|
||||
android:textAlignment="viewStart"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="12sp"
|
||||
tools:text="https://example.com/dav/url/user/12345/calendars/1234-1234-9834-31234-12/" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/owner_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:visibility="@{model.owner.displayName != null || model.owner.url != null ? View.VISIBLE : View.GONE}"
|
||||
android:text="@string/collection_properties_owner"
|
||||
android:labelFor="@id/ownerDisplayName" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ownerDisplayName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="@{model.owner.displayName != null ? View.VISIBLE : View.GONE}"
|
||||
android:textSize="16sp"
|
||||
android:textIsSelectable="true"
|
||||
android:text="@{model.owner.displayName}"
|
||||
tools:text="Max Murmelmann"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ownerUrl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="@{model.owner.displayName == null && model.owner.url != null ? View.VISIBLE : View.GONE}"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="12sp"
|
||||
android:textIsSelectable="true"
|
||||
android:text="@{model.owner.url.toString()}"
|
||||
tools:text="/remote.php/dav/principals/users/sample-owner/"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
</layout>
|
|
@ -417,6 +417,8 @@
|
|||
<string name="delete_collection_deleting_collection">Deleting collection</string>
|
||||
<string name="collection_force_read_only">Force read-only</string>
|
||||
<string name="collection_properties">Properties</string>
|
||||
<string name="collection_properties_sync_time">Last synced:</string>
|
||||
<string name="collection_properties_sync_time_never">Never synced</string>
|
||||
<string name="collection_properties_url">Address (URL):</string>
|
||||
<string name="collection_properties_copy_url">Copy URL</string>
|
||||
<string name="collection_properties_owner">Owner:</string>
|
||||
|
|
Loading…
Reference in a new issue