A fork of Simple Time Tracker with WearOS support. (Now integrated upstream)
0

Configure Feed

Select the types of activity you want to include in your feed.

add top multitasking activities to statistics detail

razeeman (Feb 3, 2024, 7:55 PM +0300) 4c1bedec f8be367b

+145 -38
+1
core/src/main/res/values-ar/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">تحصيص يومي</string> 387 387 <string name="statistics_detail_duration_split_hint">تحصيص مدة التتبع</string> 388 388 <string name="statistics_detail_next_activities_hint">أهم الأنشطة القادمة</string> 389 + <string name="statistics_detail_multitasking_activities_hint">أعلى المهام المتعددة</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <item quantity="zero">تتبع</item>
+1
core/src/main/res/values-ca/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">Distribució diària</string> 387 387 <string name="statistics_detail_duration_split_hint">Distribució de la durada de l\'enregistrament</string> 388 388 <string name="statistics_detail_next_activities_hint">Les properes activitats principals</string> 389 + <string name="statistics_detail_multitasking_activities_hint">Top multitasca</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">Enregistraments</item>-->
+1
core/src/main/res/values-de/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">Tägliche Verteilung</string> 387 387 <string name="statistics_detail_duration_split_hint">Verteilung der Aufzeichnungsdauer</string> 388 388 <string name="statistics_detail_next_activities_hint">Top nächste Aktivitäten</string> 389 + <string name="statistics_detail_multitasking_activities_hint">Top Multitasking</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">keine Einträge</item>-->
+1
core/src/main/res/values-es/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">Distribución diaria</string> 387 387 <string name="statistics_detail_duration_split_hint">Distribución de la duración del registro</string> 388 388 <string name="statistics_detail_next_activities_hint">Principales próximas actividades</string> 389 + <string name="statistics_detail_multitasking_activities_hint">Principal multitarea</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">Registros</item>-->
+1
core/src/main/res/values-fa/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">پراکندگی روزانه</string> 387 387 <string name="statistics_detail_duration_split_hint">پراکندگی طول تاریخچه</string> 388 388 <string name="statistics_detail_next_activities_hint">برترین فعالیت های بعدی</string> 389 + <string name="statistics_detail_multitasking_activities_hint">چند کاره برتر</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">بدون تاریخچه</item>-->
+1
core/src/main/res/values-fr/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">Distribution quotidienne</string> 387 387 <string name="statistics_detail_duration_split_hint">Répartition des durées d\'enregistrement</string> 388 388 <string name="statistics_detail_next_activities_hint">Top des prochaines activités</string> 389 + <string name="statistics_detail_multitasking_activities_hint">Top multitâche</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">Enregistrement</item> -->
+1
core/src/main/res/values-hi/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">दैनिक वितरण</string> 387 387 <string name="statistics_detail_duration_split_hint">अभिलेख अवधि वितरण</string> 388 388 <string name="statistics_detail_next_activities_hint">शीर्ष अगली गतिविधियाँ</string> 389 + <string name="statistics_detail_multitasking_activities_hint">शीर्ष मल्टीटास्कड</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">अभिलेख</item>-->
+1
core/src/main/res/values-in/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">Distribusi harian</string> 387 387 <string name="statistics_detail_duration_split_hint">Distribusi durasi rekaman</string> 388 388 <string name="statistics_detail_next_activities_hint">Aktivitas teratas selanjutnya</string> 389 + <string name="statistics_detail_multitasking_activities_hint">Multitugas teratas</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">Rekor</item>-->
+1
core/src/main/res/values-it/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">Distribuzione giornaliera</string> 387 387 <string name="statistics_detail_duration_split_hint">Distribuzione della durata del record</string> 388 388 <string name="statistics_detail_next_activities_hint">Le migliori attività successive</string> 389 + <string name="statistics_detail_multitasking_activities_hint">Il massimo del multitasking</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">Registrazioni</item>-->
+1
core/src/main/res/values-ja/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">日次記録</string> 387 387 <string name="statistics_detail_duration_split_hint">記録期間分布</string> 388 388 <string name="statistics_detail_next_activities_hint">次に注目のアクティビティ</string> 389 + <string name="statistics_detail_multitasking_activities_hint">トップのマルチタスク</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <item quantity="zero">記録</item>
+1
core/src/main/res/values-ko/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">일별 분포</string> 387 387 <string name="statistics_detail_duration_split_hint">기록 기간 분포</string> 388 388 <string name="statistics_detail_next_activities_hint">다음 주요 활동</string> 389 + <string name="statistics_detail_multitasking_activities_hint">최고의 멀티태스킹</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">기록</item>-->
+1
core/src/main/res/values-nl/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">Dagelijkse verdeling</string> 387 387 <string name="statistics_detail_duration_split_hint">Verdeling opname duur</string> 388 388 <string name="statistics_detail_next_activities_hint">Top volgende activiteiten</string> 389 + <string name="statistics_detail_multitasking_activities_hint">Top multitasken</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">Gegevens</item>-->
+1
core/src/main/res/values-pt/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">Distribuição diária</string> 387 387 <string name="statistics_detail_duration_split_hint">Distribuição da duração do registro</string> 388 388 <string name="statistics_detail_next_activities_hint">Principais próximas atividades</string> 389 + <string name="statistics_detail_multitasking_activities_hint">Principais multitarefas</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <item quantity="one">Registro</item>
+1
core/src/main/res/values-ru/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">Распределение в течении дня</string> 387 387 <string name="statistics_detail_duration_split_hint">Распределение продолжительности записи</string> 388 388 <string name="statistics_detail_next_activities_hint">Топ следующих активностей</string> 389 + <string name="statistics_detail_multitasking_activities_hint">Топ многозадачности</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <item quantity="zero">Записей</item>
+1
core/src/main/res/values-sv/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">Daglig fördelning</string> 387 387 <string name="statistics_detail_duration_split_hint">Fördelning händelselängd</string> 388 388 <string name="statistics_detail_next_activities_hint">Topp nästa aktiviteter</string> 389 + <string name="statistics_detail_multitasking_activities_hint">Top multitasked</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">händelser</item>-->
+1
core/src/main/res/values-tr/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">Günlük dağılım</string> 387 387 <string name="statistics_detail_duration_split_hint">Kayıt süresi dağılımı</string> 388 388 <string name="statistics_detail_next_activities_hint">Sonraki en iyi aktiviteler</string> 389 + <string name="statistics_detail_multitasking_activities_hint">En iyi çoklu görev</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">Kayıt</item>-->
+1
core/src/main/res/values-uk/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">Розподіл протягом дня</string> 387 387 <string name="statistics_detail_duration_split_hint">Розподіл тривалості запису</string> 388 388 <string name="statistics_detail_next_activities_hint">Топ наступних заходів</string> 389 + <string name="statistics_detail_multitasking_activities_hint">Топ багатозадачності</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <item quantity="zero">Записів</item>
+1
core/src/main/res/values-vi/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">Phân bổ hàng ngày</string> 387 387 <string name="statistics_detail_duration_split_hint">Phân bổ thời lượng bản ghi</string> 388 388 <string name="statistics_detail_next_activities_hint">Hoạt động tiếp theo hàng đầu</string> 389 + <string name="statistics_detail_multitasking_activities_hint">Đa nhiệm hàng đầu</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">Bản ghi</item>-->
+1
core/src/main/res/values-zh-rTW/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">每日分配</string> 387 387 <string name="statistics_detail_duration_split_hint">紀錄期間分配</string> 388 388 <string name="statistics_detail_next_activities_hint">下一個熱門活動</string> 389 + <string name="statistics_detail_multitasking_activities_hint">頂級多工處理</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">紀錄</item>-->
+1
core/src/main/res/values-zh/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">每日分发</string> 387 387 <string name="statistics_detail_duration_split_hint">记录持续时间分布</string> 388 388 <string name="statistics_detail_next_activities_hint">下一个热门活动</string> 389 + <string name="statistics_detail_multitasking_activities_hint">顶级多任务处理</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <!-- <item quantity="zero">Records</item>-->
+1
core/src/main/res/values/strings.xml
··· 386 386 <string name="statistics_detail_day_split_hint">Daily distribution</string> 387 387 <string name="statistics_detail_duration_split_hint">Record duration distribution</string> 388 388 <string name="statistics_detail_next_activities_hint">Top next activities</string> 389 + <string name="statistics_detail_multitasking_activities_hint">Top multitasked</string> 389 390 390 391 <plurals name="statistics_detail_times_tracked"> 391 392 <item quantity="zero">Records</item>
+121 -24
features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailAdjacentActivitiesInteractor.kt
··· 1 1 package com.example.util.simpletimetracker.feature_statistics_detail.interactor 2 2 3 3 import com.example.util.simpletimetracker.core.mapper.TimeMapper 4 + import com.example.util.simpletimetracker.core.repo.ResourceRepo 4 5 import com.example.util.simpletimetracker.domain.extension.getTypeIds 5 6 import com.example.util.simpletimetracker.domain.extension.orZero 6 7 import com.example.util.simpletimetracker.domain.interactor.PrefsInteractor ··· 11 12 import com.example.util.simpletimetracker.domain.model.RecordType 12 13 import com.example.util.simpletimetracker.domain.model.RecordsFilter 13 14 import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType 15 + import com.example.util.simpletimetracker.feature_base_adapter.hint.HintViewData 16 + import com.example.util.simpletimetracker.feature_statistics_detail.R 14 17 import com.example.util.simpletimetracker.feature_statistics_detail.mapper.StatisticsDetailViewDataMapper 15 18 import kotlinx.coroutines.Dispatchers 16 19 import kotlinx.coroutines.withContext 17 20 import javax.inject.Inject 18 21 19 22 class StatisticsDetailAdjacentActivitiesInteractor @Inject constructor( 23 + private val resourceRepo: ResourceRepo, 20 24 private val prefsInteractor: PrefsInteractor, 21 25 private val recordTypeInteractor: RecordTypeInteractor, 22 26 private val recordInteractor: RecordInteractor, ··· 30 34 rangePosition: Int, 31 35 ): List<ViewHolderType> = withContext(Dispatchers.Default) { 32 36 // Show only if one activity is selected. 33 - val typeId = filter.getTypeIds() 34 - .takeIf { it.size == 1 } 35 - ?.firstOrNull() 36 - ?: return@withContext getEmptyViewData() 37 + val typeId = getActivityId(filter) ?: return@withContext getEmptyViewData() 38 + val isDarkTheme = prefsInteractor.getDarkMode() 39 + val recordTypes = recordTypeInteractor.getAll().associateBy(RecordType::id) 40 + val actualRecords = getRecords(rangeLength, rangePosition) 41 + val nextActivitiesIds = calculateNextActivities(typeId, actualRecords) 42 + val multitaskingActivitiesIds = calculateMultitasking(typeId, actualRecords) 43 + 44 + fun mapPreviews(typeToCounts: List<CalculationResult>): List<ViewHolderType> { 45 + val total = typeToCounts.sumOf(CalculationResult::count) 46 + .takeUnless { it == 0L } 47 + ?: return emptyList() 48 + 49 + val typeToPercents = typeToCounts.map { result -> 50 + result.typeId to (result.count * 100 / total) 51 + } 52 + 53 + return typeToPercents.mapIndexedNotNull { index, (typeId, percent) -> 54 + // So that all items would sum up to 100, 55 + // adjust value of the first element according to all the rest elements. 56 + val isFirst = index == 0 57 + val correctedPercent = if (isFirst) { 58 + 100 - typeToPercents.drop(1).sumOf { it.second } 59 + } else { 60 + percent 61 + } 62 + 63 + statisticsDetailViewDataMapper.mapToPreview( 64 + recordType = recordTypes[typeId] ?: return@mapIndexedNotNull null, 65 + isDarkTheme = isDarkTheme, 66 + isFirst = false, 67 + isForComparison = false, 68 + ).copy(name = "$correctedPercent%") 69 + } 70 + } 37 71 38 - val isDarkTheme = prefsInteractor.getDarkMode() 72 + val nextActivities = nextActivitiesIds 73 + .let(::mapPreviews) 74 + val nextActivitiesHint = resourceRepo 75 + .getString(R.string.statistics_detail_next_activities_hint) 76 + .let(::HintViewData) 77 + .takeIf { nextActivities.isNotEmpty() } 78 + .let(::listOfNotNull) 79 + 80 + val multitaskingActivities = multitaskingActivitiesIds 81 + .let(::mapPreviews) 82 + val multitaskingActivitiesHint = resourceRepo 83 + .getString(R.string.statistics_detail_multitasking_activities_hint) 84 + .let(::HintViewData) 85 + .takeIf { multitaskingActivities.isNotEmpty() } 86 + .let(::listOfNotNull) 87 + 88 + return@withContext nextActivitiesHint + 89 + nextActivities + 90 + multitaskingActivitiesHint + 91 + multitaskingActivities 92 + } 93 + 94 + // Don't use records from viewModel because they are already filtered, 95 + // we need all records here. 96 + private suspend fun getRecords( 97 + rangeLength: RangeLength, 98 + rangePosition: Int, 99 + ): List<RecordBase> { 39 100 val firstDayOfWeek = prefsInteractor.getFirstDayOfWeek() 40 101 val startOfDayShift = prefsInteractor.getStartOfDayShift() 41 - val recordTypes = recordTypeInteractor.getAll().associateBy(RecordType::id) 42 102 43 103 val range = timeMapper.getRangeStartAndEnd( 44 104 rangeLength = rangeLength, ··· 46 106 firstDayOfWeek = firstDayOfWeek, 47 107 startOfDayShift = startOfDayShift, 48 108 ) 49 - // Don't use records from viewModel because they are already filtered, 50 - // we need all records here. 51 - val actualRecords = if (range.timeStarted == 0L && range.timeEnded == 0L) { 109 + return if (range.timeStarted == 0L && range.timeEnded == 0L) { 52 110 recordInteractor.getAll() 53 111 } else { 54 112 recordInteractor.getFromRange(range) 55 113 } 56 - val nextActivitiesIds = calculate( 57 - typeId = typeId, 58 - records = actualRecords, 59 - ) 114 + } 60 115 61 - return@withContext nextActivitiesIds.mapNotNull { type -> 62 - statisticsDetailViewDataMapper.mapToPreview( 63 - recordType = recordTypes[type] ?: return@mapNotNull null, 64 - isDarkTheme = isDarkTheme, 65 - isFirst = false, 66 - isForComparison = false, 67 - ) 68 - } 116 + private fun getActivityId( 117 + filter: List<RecordsFilter>, 118 + ): Long? { 119 + return filter.getTypeIds() 120 + .takeIf { it.size == 1 } 121 + ?.firstOrNull() 69 122 } 70 123 71 - private fun calculate( 124 + // TODO make more precise calculations? 125 + private fun calculateNextActivities( 72 126 typeId: Long, 73 127 records: List<RecordBase>, 74 - ): List<Long> { 128 + ): List<CalculationResult> { 75 129 val counts = mutableMapOf<Long, Long>() 76 130 77 131 val recordsSorted = records.sortedBy { it.timeStarted } 78 132 var currentRecord: RecordBase? = null 79 133 recordsSorted.forEach { record -> 80 134 val currentTimeEnded = currentRecord?.timeEnded 81 - if (currentTimeEnded != null && record.timeStarted >= currentTimeEnded) { 135 + if (currentTimeEnded != null && 136 + currentTimeEnded <= record.timeStarted 137 + ) { 82 138 record.typeIds.firstOrNull()?.let { id -> 83 139 counts[id] = counts[id].orZero() + 1 84 140 } ··· 92 148 return counts.keys 93 149 .sortedByDescending { counts[it].orZero() } 94 150 .take(MAX_COUNT) 151 + .map { CalculationResult(it, counts[it].orZero()) } 152 + } 153 + 154 + // TODO make more precise calculations? 155 + private fun calculateMultitasking( 156 + typeId: Long, 157 + records: List<RecordBase>, 158 + ): List<CalculationResult> { 159 + val counts = mutableMapOf<Long, Long>() 160 + 161 + val recordsSorted = records.sortedBy { it.timeStarted } 162 + var currentRecord: RecordBase? = null 163 + recordsSorted.forEach { record -> 164 + val currentTimeStarted = currentRecord?.timeStarted 165 + val currentTimeEnded = currentRecord?.timeEnded 166 + if (currentTimeStarted != null && 167 + currentTimeEnded != null && 168 + // Find next records that was started after this one but before this one ends. 169 + currentTimeStarted <= record.timeStarted && 170 + currentTimeEnded > record.timeStarted && 171 + // Cutoff short intersections. 172 + currentTimeEnded - record.timeStarted > 1_000L 173 + ) { 174 + record.typeIds.firstOrNull()?.let { id -> 175 + counts[id] = counts[id].orZero() + 1 176 + } 177 + } 178 + if (typeId in record.typeIds) { 179 + currentRecord = record 180 + } 181 + } 182 + 183 + return counts.keys 184 + .sortedByDescending { counts[it].orZero() } 185 + .take(MAX_COUNT) 186 + .map { CalculationResult(it, counts[it].orZero()) } 95 187 } 96 188 97 189 private fun getEmptyViewData(): List<ViewHolderType> { 98 190 return emptyList() 99 191 } 192 + 193 + private data class CalculationResult( 194 + val typeId: Long, 195 + val count: Long, 196 + ) 100 197 101 198 companion object { 102 199 private const val MAX_COUNT = 5
+1 -1
features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/view/StatisticsDetailFragment.kt
··· 63 63 } 64 64 private val nextActivitiesAdapter: BaseRecyclerAdapter by lazy { 65 65 BaseRecyclerAdapter( 66 + createHintAdapterDelegate(), 66 67 createStatisticsPreviewAdapterDelegate() 67 68 ) 68 69 } ··· 311 312 private fun updateNextActivitiesViewData( 312 313 viewData: List<ViewHolderType> 313 314 ) { 314 - binding.tvStatisticsDetailNextActivitiesHint.isVisible = viewData.isNotEmpty() 315 315 binding.rvStatisticsDetailNextActivities.isVisible = viewData.isNotEmpty() 316 316 nextActivitiesAdapter.replace(viewData) 317 317 }
+2 -13
features/feature_statistics_detail/src/main/res/layout/statistics_detail_fragment.xml
··· 387 387 tools:barColor="@color/black" 388 388 tools:barCount="7" /> 389 389 390 - <androidx.appcompat.widget.AppCompatTextView 391 - android:id="@+id/tvStatisticsDetailNextActivitiesHint" 392 - android:layout_width="match_parent" 393 - android:layout_height="wrap_content" 394 - android:layout_marginTop="8dp" 395 - android:gravity="center" 396 - android:paddingHorizontal="12dp" 397 - android:paddingTop="8dp" 398 - android:text="@string/statistics_detail_next_activities_hint" 399 - android:textColor="?appTextHintColor" 400 - app:layout_constraintTop_toBottomOf="@id/chartStatisticsDetailDurationSplitCompare" /> 401 - 402 390 <androidx.recyclerview.widget.RecyclerView 403 391 android:id="@+id/rvStatisticsDetailNextActivities" 404 392 android:layout_width="match_parent" 405 393 android:layout_height="wrap_content" 406 394 android:layout_marginHorizontal="4dp" 395 + android:layout_marginTop="8dp" 407 396 android:overScrollMode="never" 408 - app:layout_constraintTop_toBottomOf="@id/tvStatisticsDetailNextActivitiesHint" 397 + app:layout_constraintTop_toBottomOf="@id/chartStatisticsDetailDurationSplitCompare" 409 398 tools:itemCount="1" 410 399 tools:listitem="@layout/statistics_detail_preview_item" /> 411 400