From 0cf7655b606b33b29bac65cc76ea0508c61a1d64 Mon Sep 17 00:00:00 2001 From: vincent-paing Date: Sun, 24 Mar 2024 11:56:00 +0700 Subject: [PATCH] Add Widget to see failing projects at a glance --- app/build.gradle.kts | 7 +- app/src/main/AndroidManifest.xml | 25 ++- .../dev/aungkyawpaing/ccdroidx/CCDroidXApp.kt | 4 +- .../ccdroidx/feature/MainActivity.kt | 2 +- .../feature/sync/SyncProjectWorker.kt | 4 +- .../ccdroidx/feature/sync/SyncProjects.kt | 7 +- .../feature/sync/SyncWorkerScheduler.kt | 27 ++- .../feature/widget/DashboardWidget.kt | 163 ++++++++++++++++++ .../feature/widget/DashboardWidgetPreview.kt | 104 +++++++++++ .../ccdroidx/feature/widget/WidgetManager.kt | 20 +++ .../ccdroidx/work/MyWorkerFactory.kt | 23 --- .../res/drawable/dashboard_widget_preview.png | Bin 0 -> 23222 bytes app/src/main/res/drawable/ic_refresh_24.xml | 12 ++ app/src/main/res/values/strings.xml | 4 + .../main/res/xml/dashboard_widget_info.xml | 10 ++ .../ccdroidx/feature/SyncProjectsTest.kt | 24 ++- data/build.gradle.kts | 1 - .../ccdroidx/data/ProjectRepo.kt | 7 + .../ccdroidx/data/db/ProjectTableDao.kt | 3 + .../data/_testhelper_/FakeProjectTableDao.kt | 4 + gradle/libs.versions.toml | 12 +- wear/build.gradle.kts | 1 - weardatalayer/build.gradle.kts | 1 - 23 files changed, 421 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/widget/DashboardWidget.kt create mode 100644 app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/widget/DashboardWidgetPreview.kt create mode 100644 app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/widget/WidgetManager.kt delete mode 100644 app/src/main/java/dev/aungkyawpaing/ccdroidx/work/MyWorkerFactory.kt create mode 100644 app/src/main/res/drawable/dashboard_widget_preview.png create mode 100644 app/src/main/res/drawable/ic_refresh_24.xml create mode 100644 app/src/main/res/xml/dashboard_widget_info.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 41de66d..85ecd6d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -160,6 +160,10 @@ dependencies { implementation(libs.accompanist) + implementation(libs.androidx.glance.appwidget) + testImplementation(libs.androidx.glance.appwidget.testing) + implementation(libs.androidx.glance.material3) + implementation(libs.androidx.hilt.navigation) implementation(libs.compose.destinations.core) ksp(libs.compose.destinations.ksp) @@ -200,9 +204,10 @@ dependencies { implementation(libs.permissionFlow.android) implementation(libs.dagger.hilt.android) - implementation(libs.dagger.hilt.work) ksp(libs.dagger.hilt.compiler) ksp(libs.dagger.hilt.android.compiler) + implementation(libs.androidx.hilt.work) + ksp(libs.androidx.hilt.compiler) androidTestImplementation(libs.dagger.hilt.android.testing) kspAndroidTest(libs.dagger.hilt.compiler) kspAndroidTest(libs.dagger.hilt.android.compiler) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0ee01cb..0bd0800 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ android:roundIcon="@mipmap/ic_launcher" android:supportsRtl="true" android:theme="@style/Theme.CCDroidX"> + - + + + + + + + + + + android:exported="false" + tools:node="merge"> + + + () + .addTag(SyncProjectWorker.TAG) + .setConstraints(constraints) + .build() + + WorkManager.getInstance(context) + .enqueue(syncWorkRequest) } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/widget/DashboardWidget.kt b/app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/widget/DashboardWidget.kt new file mode 100644 index 0000000..e2918eb --- /dev/null +++ b/app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/widget/DashboardWidget.kt @@ -0,0 +1,163 @@ +package dev.aungkyawpaing.ccdroidx.feature.widget + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import androidx.glance.ColorFilter +import androidx.glance.GlanceId +import androidx.glance.GlanceModifier +import androidx.glance.GlanceTheme +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.LocalContext +import androidx.glance.action.ActionParameters +import androidx.glance.action.actionParametersOf +import androidx.glance.action.actionStartActivity +import androidx.glance.action.clickable +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import androidx.glance.appwidget.SizeMode +import androidx.glance.appwidget.action.ActionCallback +import androidx.glance.appwidget.action.actionRunCallback +import androidx.glance.appwidget.cornerRadius +import androidx.glance.appwidget.lazy.LazyColumn +import androidx.glance.appwidget.lazy.items +import androidx.glance.appwidget.provideContent +import androidx.glance.background +import androidx.glance.layout.Alignment +import androidx.glance.layout.Box +import androidx.glance.layout.Column +import androidx.glance.layout.Row +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.height +import androidx.glance.layout.padding +import androidx.glance.layout.size +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import dagger.hilt.android.AndroidEntryPoint +import dev.aungkyawpaing.ccdroidx.R +import dev.aungkyawpaing.ccdroidx.common.BuildStatus +import dev.aungkyawpaing.ccdroidx.common.Project +import dev.aungkyawpaing.ccdroidx.data.ProjectRepo +import dev.aungkyawpaing.ccdroidx.feature.MainActivity +import dev.aungkyawpaing.ccdroidx.feature.sync.SyncWorkerScheduler +import javax.inject.Inject + +class DashboardWidget( + private val projectRepo: ProjectRepo +) : GlanceAppWidget() { + + override val sizeMode = SizeMode.Exact + override suspend fun provideGlance(context: Context, id: GlanceId) { + + provideContent { + val failingProjects = + projectRepo.getAllNotBuildStatus(BuildStatus.SUCCESS).collectAsState(initial = emptyList()) + + DashboardWidgetContent(failingProjects.value) + } + } +} + +@Composable +private fun DashboardWidgetContent(failingProjects: List) { + val context = LocalContext.current + + GlanceTheme { + Column( + modifier = GlanceModifier + .fillMaxSize() + .background(GlanceTheme.colors.background) + ) { + + Row( + modifier = GlanceModifier + .fillMaxWidth() + .background(GlanceTheme.colors.primary) + .clickable(onClick = actionStartActivity(MainActivity::class.java)), + verticalAlignment = Alignment.CenterVertically + ) { + val title = if (failingProjects.isEmpty()) { + context.getString(R.string.dashboard_widget_title_green) + } else { + context.getString(R.string.dashboard_widget_title_red, failingProjects.size.toString()) + } + val titleStyle = TextStyle( + color = GlanceTheme.colors.onPrimary, + fontSize = TextUnit(16.0f, TextUnitType.Sp), + fontWeight = FontWeight.Medium, + ) + Image( + provider = ImageProvider(R.drawable.ic_refresh_24), + contentDescription = context.getString(R.string.menu_item_sync_project_status), + colorFilter = ColorFilter.tint(GlanceTheme.colors.onPrimary), + modifier = GlanceModifier.defaultWeight().size(48.dp).padding(12.dp) + .clickable(onClick = actionRunCallback()) + ) + + Text( + text = title, + style = titleStyle, + modifier = GlanceModifier.fillMaxWidth() + ) + } + + LazyColumn(modifier = GlanceModifier.padding(8.dp)) { + items(failingProjects) { project -> + Column { + Box( + modifier = GlanceModifier.cornerRadius(8.dp) + .background(R.color.build_fail) + .clickable( + onClick = actionStartActivity( + MainActivity::class.java, + actionParametersOf( + ActionParameters.Key(MainActivity.INTENT_EXTRA_URL) to project.webUrl + ) + ) + ) + ) { + Text( + text = project.name, + style = TextStyle( + color = GlanceTheme.colors.onError, + fontSize = TextUnit(12.0f, TextUnitType.Sp), + fontWeight = FontWeight.Normal + ), + maxLines = 1, + modifier = GlanceModifier.fillMaxWidth().padding(8.dp) + ) + } + + Box(modifier = GlanceModifier.height(8.dp)) {} + } + } + } + } + } +} + +class WidgetRefreshAction : ActionCallback { + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters + ) { + SyncWorkerScheduler(context).scheduleOneTimeWork() + } +} + +@AndroidEntryPoint +class DashboardWidgetReceiver : GlanceAppWidgetReceiver() { + + @Inject + lateinit var projectRepo: ProjectRepo + + override val glanceAppWidget: GlanceAppWidget + get() = DashboardWidget(projectRepo) +} diff --git a/app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/widget/DashboardWidgetPreview.kt b/app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/widget/DashboardWidgetPreview.kt new file mode 100644 index 0000000..7b7b6cb --- /dev/null +++ b/app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/widget/DashboardWidgetPreview.kt @@ -0,0 +1,104 @@ +package dev.aungkyawpaing.ccdroidx.feature.widget + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.google.accompanist.themeadapter.material3.Mdc3Theme +import dev.aungkyawpaing.ccdroidx.R + +/** + * Glance doesn't support preview for now, so we're writing a minimal preview back in compose ui + * This preview image used as widget preview image so changing this file require you to update the + * widget preview image + */ +@Preview +@Composable +fun DashboardWidgetPreview() { + Mdc3Theme { + Column( + modifier = Modifier + .size(150.dp, 225.dp) + .background(MaterialTheme.colorScheme.background) + ) { + + Row( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primary), + verticalAlignment = Alignment.CenterVertically + ) { + val title = "2 Red" + Image( + Icons.Filled.Refresh, + null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimary), + modifier = Modifier + .size(48.dp) + .padding(12.dp) + ) + + Text( + "3 Red", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onPrimary + ) + } + + val exampleProjects = listOf( + "lisan/al-giab", + "anakin/sky-walker", + "answer/to-be-rich-is", + "ccdroidx/ccdroidx-pipeline", + "you-cant/see-this" + ) + + LazyColumn( + modifier = Modifier.padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(exampleProjects) { projectName -> + Column { + Box( + modifier = Modifier + .background( + colorResource(id = R.color.build_fail), RoundedCornerShape(8.dp) + ), + ) { + Text( + projectName, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onError, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + } + } + } + } + } + } +} diff --git a/app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/widget/WidgetManager.kt b/app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/widget/WidgetManager.kt new file mode 100644 index 0000000..fb1385d --- /dev/null +++ b/app/src/main/java/dev/aungkyawpaing/ccdroidx/feature/widget/WidgetManager.kt @@ -0,0 +1,20 @@ +package dev.aungkyawpaing.ccdroidx.feature.widget + +import android.content.Context +import androidx.glance.appwidget.GlanceAppWidgetManager +import dagger.hilt.android.qualifiers.ApplicationContext +import dev.aungkyawpaing.ccdroidx.data.ProjectRepo +import javax.inject.Inject + +class WidgetManager @Inject constructor( + @ApplicationContext private val context: Context, + private val projectRepo: ProjectRepo, +) { + suspend fun updateDashboardWidget() { + val manager = GlanceAppWidgetManager(context) + val glanceIds = manager.getGlanceIds(DashboardWidget::class.java) + glanceIds.forEach { glanceId -> + DashboardWidget(projectRepo).update(context, glanceId) + } + } +} diff --git a/app/src/main/java/dev/aungkyawpaing/ccdroidx/work/MyWorkerFactory.kt b/app/src/main/java/dev/aungkyawpaing/ccdroidx/work/MyWorkerFactory.kt deleted file mode 100644 index 6bf48f8..0000000 --- a/app/src/main/java/dev/aungkyawpaing/ccdroidx/work/MyWorkerFactory.kt +++ /dev/null @@ -1,23 +0,0 @@ -package dev.aungkyawpaing.ccdroidx.work - -import android.content.Context -import androidx.work.ListenableWorker -import androidx.work.WorkerFactory -import androidx.work.WorkerParameters -import dev.aungkyawpaing.ccdroidx.feature.notification.NotifyProjectStatus -import dev.aungkyawpaing.ccdroidx.feature.sync.SyncProjectWorker -import dev.aungkyawpaing.ccdroidx.feature.sync.SyncProjects -import javax.inject.Inject - -class MyWorkerFactory @Inject constructor( - private val syncProjects: SyncProjects, - private val notifyProjectStatus: NotifyProjectStatus -) : - WorkerFactory() { - override fun createWorker( - appContext: Context, - workerClassName: String, - workerParameters: WorkerParameters - ): ListenableWorker = - SyncProjectWorker(appContext, workerParameters, syncProjects, notifyProjectStatus) -} diff --git a/app/src/main/res/drawable/dashboard_widget_preview.png b/app/src/main/res/drawable/dashboard_widget_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..b08bd7ac8663e208de7941830a80b1483e70ffc9 GIT binary patch literal 23222 zcmd?QWn5HW`|pp0NC?s*4MPY>hqNODf^>s0bSO21Gz}AT2d? z$JxNw``q{W{m$z=`Jexj^Te00&7QsYTI*WZTA%mzS%J!mFYaSM!bU+sxi2dNQAI(y z6M=$)s(bG)aHaP%Aq5J`D=%3HSlv~Ba~dm-ayXj#bnB1cG%svMzClO7Pf!dtrQd2O zkJj2R;3rKQH@t!1df08rC4%}Mg^u+7o$+_tEv(L^IB9idNDo=YoU-V{0#NkJ+=yKD z3|3i}H5!(P#1)9SwCPlP#+r8n^5{LiEyN5W{e{SWp|z*sG;gwooSr_tHU|p=fv9Bv zKlg9a(Cmy`$%)cY@|(+snUw@@&(-I9N&cMNvmT2)d(-u??pGHnRD+jozGQJ%p0hg3 zv$I~guf*BUldfEmm zO*Ts0&&+u(rd9V&%Zhr+X~fO&ymu`*zn{;FDXFdZEF!l~qlGS_J>>fuIrH2gkffM| zek|J51-ym*SdRpW(6h1pH5tN_D5IdP#C37^3D2uv)Ko6K<4B%!5Rc;~;oeTIy`}ig zw?ut!sQ!u=ed{#x_`Kxm2b|aIz<6(6%On1(aNtF+^i62zL{~#BJzbTkv){XR1sBO-dOS~_SzgzJeN|Jo zX(jmiiGsO@ny*f!NzjqCX;-0p=~uVd@xc*GJj$oEWU?gPlO@Og{n zUj)A02Q*?Pg{#^Xqhar7lDZQI{o}FNSbI8a6f)nyg7g$}Q#_TYwYighpl z(9fQ2dIGBiMVQHY2GvINX(g#lQQ=oB$%8#hNKs2tm_9=ve-Z8b{G1|w-@MGY8G{CM z_|8Vpvcy|*6@uN!vn!XUzm#^6trKkj+yYD=JAA;|CExS1Jx zwRkWW(dBP{+I8g0%oCv!rMeZk*q8>3t2-+ZB)Cg5mGfn%Q_RxKzF8LJOs>a2z8*j~ zYOoe3lg*xuWrk8$x@~w7gb}$Yv>g3eq%!^vk%Zt239qD+N|)qsF`=l)@Tp__t9%8E zk|;;|6R#KZEz_FyW|Lb&Ii^I(fWB(jpoONS5vUZ=qL|h9jvQMF8u$!+&@?bo>3o@edW z?zRrp>~LPxfgdgL*nbh*7UvZ-CImljgZOy)A8~$bKt?0tA0XY^eO!c1>pYtkK%7d{ z>|{s6=Yd!-&F>4#OKOFe z*fcn4?1^NNMFz1!9#q(-ViXDMGYQq!%bE4(R^4lkgrM26ieDKg- z=bGKNkwuSt0ne!DX|=tSOW&N9`o&YOMNz+~*%Y&?DC>+oMnYVEhOS|xM9$h0gDt&V z*+St=LT=YZyiikPu=ecqiw(8t`AyD$i39=|~vZ6JpJ{HKHVyn6BLm`(t|t3sti9 z_yB&Hm5542{4&#~jfy|H9G!%6l)1q*OA8b0_{-=P{>G0vGA2S+Azo!wFQIC_)xSCWvdtDEW`$m6LVS=YoD}oDs6AF-j1FZEOqq8jh94HJ zp(EuENZ+RU25j~Zz-pbl*E zZtmSL!reJOl0?ZUZ?l6tHDYXaR;I^08}7JrFmRN>ELxP7rvfIbm zE_JczT+fV^w_CRN8r+V!%R@X{6hOzp4@nIQM@KeKCKrwM8+pU@wrTZA`Ao*w+qBI} zQf1}gA+(;MA3A-2X4fCJn^{)jlyzBHa%L_5_RsgJ>7qaOub#jIg0`_M!Xb%pD_xIt zgUI9Jm{Cz@+qmGI5`4Q5Fk<`iE0Q+FEZkorV}cj1ss1+n!V*8Hq93>a@g_g|U@nu@ z?xYg(<%`W=cJk{#+^W(e6@`5t4Ygn@`9OsX3cUS>%d{gm^ZxFAx~`q&lj4@ao-5Z? zRE-qum3lg~!FsZ&Vy&rabjApImo(Q=mh8(9BzqcA-GRVG{1kDvybo*NF@X6(dOcra z@n9DlZf=V4jOA86Qm{jngGfKyvd{u(oSqPbaCbG9hM7J znR8LH(+GT4Y0}4mY;b+|DYK%X{vtLPp3}64Ckm=OVd!7>SC=!Cs)!ewZJJU0U?uo%s5dgPFoTH8*@egsx~OH$Xy zXCq43Thd#{=NSCdWcr4PlSqlY(hvkT{2fq0g(8!1JO@ITy;6-qpfG} z3){96E{la0`$(hS8`O9evi$BRLD;00HIW_W8+IONc_UbcFwhcPiukyD)>le}MG zQZT3+^FTA;P{Zp7G2{5Hwk3T6g0e+SXS*aDc)lkg7NJ-~4g0Zt{1&PoM*>rxM>7b{@fu1dbhG|$f?Qj1A91xp2RaFSfGW12GoD0~6>DcL6Fb6FT z>SAEuZ0_0$!z{!n{H-tCm%2~sa{a<0@ATaa6l}IXF#dkQ;`do=;)*lsPnmjf!%$#D zb)D>0g&3TGpsr@4rb-+9vy0DE>~;p@QXUNP&rr!IHx}<+t*+ALY|c*2h;8ejVBZbh z;5SWul{q!{o5xQGi`A9-gO~pIw;}P`ytDZ;^*WleB#6 zmi3=$p$6o~sni%`_*F#I!{O>}94YHgFua=7XU0FErFw$s7tTQjc3S@7zGT+LN9uw0Y@Ufp3&v+NDDNYxOt(U*%_tX-uK31^w$(U zs%EqxC{JVOy&+1h`5E$t|gG&^bde zNcgPs^Cws9x|v3Q>7;JLC)T9Mk2tX>E`EJ+{pnbly#x(NltE#*>hw8!>XUpnwtZou zBPxT>S{3%JkHa%L8`+&|Wv|>PLCoivMn=zJ>N8^xZ1W?&GDVS#a-O*V;u{>GJ~t%9 zu<3n0Sr6>2dO}YxM$Yc!UO8V0&2)sK@1=MonO*849c$?Xwj*pOc@iFc%#be%eVTT7a3cRr%-_+h>qBcqY4W-J4Kw zXxx!S=wXfOyuykN!o?j)kc!AP=x4)32BiMMHsL$rOis(eg@Tvd0|VOAnck`%{~0Yc zK+MYFaVlI6;OaabwnFs{UHQz5#spvf9PKpyLMhQCxw`SZhl)AJ=>dTX3jh0n02b{3 zC;$CKApviFKtKQ}tcjz_M*!mS}WU zhm<&;(3dV&XLWxlRa6m)3J9p@-q}zSHeMRk?~bxB)Bcn!<6K~1#)_<)#y$JxGK|MS zpKcfsA6>V#ZJZsP_ffM9GN4yE&b~3)hAVN7X^BZdpz<^zE$CFt`fy_^ciuXUm@#}q zyQV)_q&#tg4g!gyX;yF6B!}Uo%>|lnuF&i>3K6KFKF}pFF`H6&)kr97<>;GJi&QNC z4S_6@8gI^wOta!KXBY;WHf=Xrngj;0kgxlD<;>fuVn@iV<*k$o1qEckd*HZErj_gZ zfVw|1-9D4`N4PlEEBKuACUHrnkK1<;evlRc{o@N|J- z;Ht?!XUL)P_uxJb#srz%ls{PlpMa}4-?dvUVhQCy4_)n#C9YuQU*jO%z;9{hffM(DdkeoMM}i_`DthC-3A_sk_=NLar`3v#P!{yiOVt(yc)XS> z0~&LLObp&?(ajB3CqM46>LbEr=ym$DVkc05XKa$FBb+QDS{f( z0|7uECj-nt z^C*^M$NG4emwz^+cvgd(WcMkM%Z-iczkk3|ymxo`$DQ>ke)Ha*(L8tPugu>DS%)3F zWXT$PQ0zS@iCS7H5wPGfF%im*%?;JsOXkkhi}jeFznRuZDl~6a@lU8|y$eg(fLZY_ zC<^Nj+dtlL(UAWrst9bP<8Hrb5vNap;;#1=^nKsu1-~U+OMQ6da;Sb%+_5GB`DzTA z8;9-o1z6>{xx=+P>7GXC+Nd#Q98RaVoo1gD?jT;aL7ulXLyTp4o~GqkESGB~j0-}o zn37!l$l=ZS8cAHmQ)Sd-?wgx~MYTi0LOT|@it1Gkt*4n6%f>}n;a^hZTCmy@yq-Ln zfm_YiiR?=*J)MkLbXjUziFfNEF<*AlinnJ@NnK{9oAru#5*{qR^_ushGB#dk)FArx z-XkZdJSe8PF~?|!9_fZsHZq)ZQeoD2Ac)W^P{-FNu&WHFS;v4?%ZCY7&aCp2Z%&pn z+lXNvZt{MX%k7zmdkyo{2R2-4tnK~Gt<)D}2o>+nhp+U6^ff%O@!CW<$2+BXE{g4i zt#_FIDee0fQ&jLUV_D9=qJ``b(b*Q9$0>N=XyM|S?7ZGY#dUu;sOO8sl;QHMhsDI@ z7q?B=urO_>J)uhqIwPScQkPkQ>+EPtA+a*g{NpI2PN6T0!Oh1%mKUoPEdJ_Xo2#Gc zgE@Gw*}-k{v`<1MkR{uiTq`Z+qwGp@vSyLQ%ELfcM(5qk9Ak*iEzMz-o1mBcd=fShFzE$Itc1VfEpmeFr3$F=2osn8WnF4F%P}w*a=|>CTH@=yn@>X7drvYn7)_1vrBRgFuqXIMS z#|cT^Y1Z%LzVep`Igjf$OqUJWDmI*oYcC(w$Q8&XxQ^t^th{(LU7^uaSv^K-5Qn7A zeWRt9UO2N8Z#Ozp;uLi12@i~v(c2`7m)Y?75b;N(&^@eSM{)-UuAg6yjn>krCdA() zjuZ-*AZTJWU)&9&iryP?XAQZ^8MPVW-0V?UgQBCON`!qavz;wWZUDsD^1-?=>YP)K zFp(m1i{F2YagwAMzsD5b7+wAa=~(`xUJvM-{x4=^AWu=f^Id(M(3(Z$lQJzGB zU0#GyGGQnyGtO$cy&rg$4iow|eEL{MrHDz00vSgGX)34-7dIpRunxe*-&-&_Tux2G z4o8ra#|MTQm}@IN-!Bum)i{JWZQ#oEVCC@jc=tbUZ=4>;m{E1qBT9S=7cF%?tLuHU z!$5X|b7bgFAT1sIz7W$6!SJdnW^f>S5{s$W+Ye)Hipo?g{@que_jnysWmH|53B`|&c{w3>yZ{xec= zFpNQHc3BK0uHE0vxUv$_S8?p%h4~Q4rmJ(+lJ(|biMIgb=tzBie^?X;m00E27EVl~w zJbt8Zni?JmT6GKNd`aKfPv>P*IPg*wME>*v z-_q>PM+H!{%WO=p|5k$o`~lhIeAfZhoL9^=n-x#4+d)g&sR)_+EAL@buRFiY=%rFIwbeS3w=IE~2r$0rj5zrGqKpc_8&ljQq78GvWUE4oxQ#W^)Eo$s5I-@HupYDaENa1Lq{#5wBn zVE9ukrnWc#nF#lx`19LPXAO_*#-G%xKY8`4ZWvi#a2o)(kuG4-kUIX7>0U~0Ni^D8 zn&4;X@|1(`E6lq(QNXy21~%K?eO^Q_BoP2K0|@Z+-I=uS2~@xMK9Jott4r;RgVEbp zEBO%0_Iq$?r%>wLS@7&|hp2FcYda(z8?77RG9auecQYPokTrTCd#Q4SZSIR4xQojp zk)ajVWqD_NZAlKs6Wg0`Fa_dV*BQu8+YcTs_@3-F;ccznPG8SS7#4!<2U(CN>v~c@}R@)Mz8|+c-3uwrWsZeqdvY2HQ>TWgHABcLQo42 z1LZ*nr53qCBs$w1k$U(l;@3&yI;{H>v8Lf>b0_AFFx2LVth1aYP+4G?vkkG{8%;60 zqIl;MDdW?%vLz_Mwb|w|>pT-7^R_{p8!ln^pmr!*j84jYq7Q=b?NX}S`@tc3v zCsp2UM%Wk?^5S85w%vQ8%q{^HZFvyJyA=jkP86{!QnB5zBOaQG2D3UnF74U#v#mpT z6ie@z-pTX5M1y_^^U4dA4I3Yuh65skC$&xdS&}1H$6e+O4~E{+zIE$ zv0G^)_X6jE(nh}>=Eo!H?b4d6moa-HXnNxo9(h!-kCSM+#t&U}{Im0bypd-*ap~=TRnz;a%*~b2i9QL{or#YS z!j9HYMwVy+0ohfS%$omfmg|M5V7^2ID!*w3x^A}5rm%He-rXz-O$uOF-om}R+EhEn zzEF-62qKL0Xj8D11b+YYL8RFiXU6Z468I|Fe_k0FsG7bJEhfNy#SemXorm+DZoV`Q zL?=C5=<_8cAffoH2NEi6_!vTk^lXTM@4hQ6&JUaXHeV_i$V51&y9KAnBo- z4kCPQWhKC-?;YqXLP*f^uJNH(?PWTdn%S2pQR$3?H2P6G+jC-`&d(vlcZ`?MG2NLi zON<^$DS_&fy2#=g`^0RRlD$4n7(pQWpRp{G1_;^zv?A$X4r|6q6A7~{2yE}Qv5F_m}-67pwdeRZjgsgef96%@! zR8)3Y#|t#Fdv1Vvmt))F)BHXC0|M9sKI71Mw_7KP{|bud2_cpHbt6D^E_OU_hDYRc zK!DF@p&f(n1f^L)n@taOy(eYqOc9?M{Nr}AoYX`D0s?-J^1PF zgYq3fGT$}c@FOtDIZpiM!x(Y8b|Oy?fndDT)@`U#ZTbC;7!(eKp@2!vg`N#N5Qk{o zWZ$(~7eJt7nf_5X%h0`HX+i>(l1zO#7(62=D;hq8`zZTAMYG2dU&5z_P!$Sk2;`%R z{ZH=9@@jAkppg|owG?#pto7fJF9E%L^Z)y@v(bPgzMjIL0j)XzOg_ebBD$Fn%TFSP z0)Qed--``sNC1({6ux1#W1F$)2Q=9}SmGj9SW_fIhQ3jE!-3&C2|TilxlID57yX<= zcdzfF`zHx}n1%-iyzE=U$IQ9a)TieQT0NimMg;4bW9jlQg^O=2BA=fl7m8+=zU-hvFQS8}g2P^$bf$}2!vdX-ZbMcq> zM6zV_yXaE_P7ao03Rar&fa)(?BmS9BQ7G_PF4k4R^CUh2>C<%~UCkXHMZ*-q(L9V5 z@XZL1Qbu?5OJb4M0k+ZLc9B|&(Fsmp?g>ZL1dJGuNcO~t{?Bv0RZORT zP%TmZWYR)WYY%E)JS?eV)iA~&8s_%j|n9B z2Ho{m%deCgc2#+on^SUxPks!GjtFfEl6lmnES~G_Ug%}KMQxqNBw?nsIxH`9o%~hr z;j-#~@%l2sZ8O^ZP+>Gdb|vZerku_T){F~Hj$JY7w6Q?b^PK7K?)Adm5Tf&jFEqn| zblG}3Q@9r*C{$ExNRP#}GD1<88R6w|_>HRgKjdx&Aa|ojosqf+BkSgizx?Bek+pAL ztyg3elzNSFlx{_a)*iyEsUL5xU^}AHqolrwxj1pr?9qaHe$SU^C@)kjV9xQH(>>kb z%~`Tylg>FAC|Nm8oX*qCRYa9}uG<=VK$xJ{x?Uw;vgV+Y3K#ZnBDz7U( z*QdkjYG-6}Rd8V7xyBd*XhPMtshQJF%j#aAK#2C$2{Y>$di0Y8)}fO68?jvJyx(3H z3Dne5W7q93s(*fYcdc{td{aX=0cS$ehU^9!g^!QHjetf<&W))Ejh?bKV&JdyVz`i< z+1NL{7+4v){GCSOj=3b78n+x2hNMAF&8uNyo<&)>)jde7n zxqnZ<=Lk`0ce1WN7Pnqd6#BI;o}4%Lv@M=$EFuGVxv3knTX3W#w4nnLnSh?p`Qdl2z2U2EZ|#G zaU4!&n}l)!`Z-5fMi1Nr41rhsE43F6NuuE4!1OnpYeO}5FPK&)sYsRWnCTL+n3KI; zy`r)%(hJQqA$FkB<*3J}*@|!(&X>sx?F-Ql)Te)t;<5h0-jWKO@;I9|zGF!khbhAM zMyP)t=6w2R2|=KNfA|uE3;AQUss-QD1Rp!SXMNOd^BX(~E6YVtgp5NbkLYd2@^#Cm z#qY$5U1gQIRS`<_e$?~l9PQY0ppkjaw1-pZv*bxr1Dfo>20K|FEewn@qS7YNFS_e( z3R)*Zb5qRuMqKG0=q!feY%Fz2AXZ;=e7Mrm@aM;p7&9w&pB;BnS7 z175$C`~4uJ*vGM7vewWsRQPz?-PJh7I1BI!!_25+Q{<>OF0rbLk!Ww2g;4Pd)t7cF z^q3J{UbN#bgoEB5K=Nvu{tV@TVodf3xSG?iASPhDd+nK`WsQ~m8}dDE(R_t*OYZ5~ z{b$tcoX%4_(*Qy8JgLQ&5_oTOewsH_dzi@-jiUFr4Rh6Z>tPK*3QM?VV{3HrW9!pA zw!yg!5=5Pyf_HP>lSqhUA&0Ub35UdKh#_WFDPFW~)d%=tml;ElNC6-voZ7Ca)HT4@$`E_F3}ulvYKSJ( zo}b)ncW!iKtj601J>S!1TD@;E8r%151iqhKUO1bXi5xF`JRI!HMCCz+&}3H{?%Ky? z4CZQMSro3&^P+LxhQ&Iic;&AhZ>)HL+p^E)SxOWwS|Je_6UDz2h_xcBO2E9nUSVI+ z%l~xI)&S&6qs<@F4j>NrLJ~6d*Iiv@_JYV4s@ZTB^NK^JFj5RiNMikBHzZ>60mNPd zMkdN7g=8FWj{e+r;g{!x7}M&0$kxpfEe2ETT5)Fyo_X#-jySPvJMSFY=SMa~CWLtX zIj|JbJ{J)K341ocb#``9$}ee8{`NN@@sh`KsjEiU#~wfggywlo&*WKlkxu6FAXHly z`v(FA(8!wSU79|McgZ2X6VqNszS%*2ByWqme83Nu9Kg33r)aC9KwnJsL5rlJkik!l ztZDY&WbC0Lk7|=bUG6SA2*jS<|B5o!0%tPC7xBfz0#7UK$%3BRp8KqjUAntGF-S-d z-`ouvNG3~76j%dWAKe3&f=uj^u(AClR!Hf1vALvwLaH*_vqJ0J7 zTb_2>gaRR2`3Fu1KoC%?Kj`LhvM*Z_9)}{)vTM&O)WzOjMep%8M%R%|l|=bTX_&VT z$L5(eF2T`6j0`<}i zU(CDglt+eH@0L^xOVf>hUs^j0qY_IewuV*oeQ@e(7>(x+r~ELfHIAgfm$cUnsc_`` zIA<7Mrw8<3EO=;XB?8kU(`To2u|%~na(YR8`*((TgQ@!p14UWK*OT^jLJGT<;JkxW z`zF0D!LVtyaN5n}*DTIR0kUF)i<0?hvH|_oW@x+tr&C*H(X_|5o`i>au6{o+g`HdQ z^FouZ;iLEF;s{!|`KR|yc26wKFXZJxwoy>}MqeSsTw`iJnzPHxsMN?tvjSM%rCT{R}>^E49gL-cU0Mpyo zL%4-j>@n7ETN3jhDF%af$Ld+n!#%9GIprw}hE)%BBsfII zOuWxHU!-^CO(&7HeKy$W3%DR0;wSxAf*Hc~d|behOwb{0)X~=rnU@}Cj3^*#%c8os zo5A)&@}VFtsqOee$;2ldUdh1ZK0#BsqkE1HtlL^;G_KW7E|erx6h?VoLcJlcIEnMKY`oS zDqPtA>5UVw_HVxCQsd1Z7x4SS`_=sfmwao?y8h`F!rCY%C2RC2Qz2icr{FnV9CBCZ zWv^npXxIK(vfnY$A}*+z@u=fE-GLQ5_RR1^t}kmD_J~`(rw?7S^$b7s!pHEFVCAUS zdD`f~2=x^<22!*AplS*x05jEA`B{4v#%Wy%4g2!TOlw4U_iaO&I}5mRaAo)5N4>U~ zE+m?4y4gCZmCY=1ZK97$h;xB`6eg;2{^n~xJP|^gK^ciZ3$j#)P7htup`zfI0$_n znF`&AC9c-`5`58E@KO=+x`QpP#?(2Tj6R%r#>>DN2*vx0DRW zwm>mN`AO^_UheQJm5RV-|GcP^qiLI5>KabF^915(oeA9NEjOoOLN@eB}$AOBwYOw#n9n(teoL*`A*vk*|Yw`I^b zZ?but@UPzAlvDAV7h_sCZ~yR~eDv;BFf+hysQ;E|kAEZuWWNO+q*4Jw6){qzdaXsJ z>{cb>uSv-bEkZ4|t=EJBg?$p@zYNHk9s1gj>QJm)1z;8r0~$qQpQ!JQXF!Os)DLSU zZ602eA>-oUu0qD^nz`;9KbjrMa(kfS&EBjpTJ#SDD#j!O02br8T9;-R1Qq9WJ9Y`E zGMgDSh~HMZzuCZu3{*k%tndCbAg0R7kt+62A7jXY`X_25p+12DgnUOKiNeMJfBC3wW1&Y3=i%Q|lZBrC zrQlM@UR_+^N-s|`siZ83Pu7sbHvx<|F)laJceU0*V-`#LH@Q$4>HZ(H zo5+a-fR;#eF9!bXN{cG_4ec@Q>n+Hvxgs)e4)@Qd0(?8aXk#?5{$LH3I zZ}jV}-jpr}I0y@M^T^2ZT@sf_&3?#0m3ut7i>2TV^=SnvNIS*_!2lc7rhij7X$=gA zf(;)5K{fy7zCl8apD>Akq-xj=#ggB&ubS$6g(N8?5A^ zkzua%&{Wj0r;o>a1ibo{s{>51Q`KMTr~#M*#wY*c++*4cfa5Jc7hHNL`FVhd>Rf>75V-gh7Qa(lD`Cb1b!KQYl3JtCoh7^fWo)I zA_OXbWY^w%&e^f9|B8X2@o%^Oyg93}ZROMO2l6UTyi?O~ru`qnK0i(gwLRbfNry}K zwOe9xw#+=5+^zdJ0BPhwzX2yg?DsdvqdD)2&p%oWd+IIG%bZEmxRm9pVb{3Rg9o@U z@xE>P3JDq%tyTYk5aJPGIbr<;K8=R|0zSBpCM|lDonC^qltBG}al-QKX!DK0g3@~7 zS`}?57E^?wJZOmVn);C7R`h)-T=vC%BoE54+Wlyec;8pzXp6_<>hcl)EQy;uS2#Lw zqJhKc8Uq;?uYRB7o;2+TJS6}8<=;3^gW|t)AmUpNw7Kg5=Xf0x|L?fROHn6lC|IIA zG2U%jV`ioGs_R5W;pyID$Yw+DBwR=-_vvr#(xQwO9P~|ui-ji_o3Hc`gl8*?sBSaS z=>qsgj;uOZYCoVa)Og%cQqpniFP|TF6VJUC)RBcF)*7IV%E1Vw`)gHRe{+n4sVenPu z6Pu*JW{L9OT?aB<$?GuZqE|W8eDqIk$t6bQR=kW^i`k&0aw=c0`XG_e1oS9gmoq3u<;uB2)()X zVO(;EH+*TZIvE(+Bhooo^UOSfB0U*B zP_=72({mn2IQqMUAf_ai{!%ApOn^G^T)p}#dTjY3R32vlj4@5Vumk}hV zMf~>rz&KDgyrEHd>;W3ZscoCcvyaai z?O%pt4BnZ2Ux!=x&Au&MQ@EllZ^Q@M*xPt!?G8J75vqFhXm9H$dK zFAEj3Nzw*%uQnq>tdP-)R%t7RR)n_YOtF4BZmKmkzg_{uWOcB}F6_|h)a5hJ(P83n zjW91juKLdnF(VF_nT7?anA5%b=bmA^ixa%v<}Wf>DL*=OV)`;OZ52^0*AK;;sNPI{ z_kpY8%>Ip&DQdGuq+Xw2QN2u0+o`Ezkh~@f&K5FWGwZaYZD6^tdg<cpuydkqWo*J@4K2CF|Fk`#4(yOP-16Gd$DLjhO_J zgKJx_zKa&T*-2cE-$%%R*sGI0(KpG^r9EioIf#2oox$7D>$}ybz3JE!q)~da_HmHw zI%0V%EMm4QQVIB|={FNedM`?vohR1A+R(8%n(eX{MjKnO#fzYeA75CsK^z_w(%L;c z6@ZIr55GTka~!~XC~6()R;sg%z|tR+Adrr_gyX%L3@Uc(yjybk^+orx=q=|qrFHAGNWVHf zJ*P89wJ@m{=!iMGEa0~(6c{!h%3z8B{7k&vhhAO8#IMveOXg;*(1;09H~G}| zC9kTgUAav2q25n{Ae^`O5Hj5s%zh@b)?ojdHK zEC|>jc^ct~>v7)|>u)L$DJ-*(4!XQ_5W<>VA`?a1MLedjIx(AABFiX`{VKxn56_B{ zzgRXyct+7DVwr_Cy3k+g3&35JHkd_SqxTq3>dan(oH@CoQqAQk4$BRE3g;K@8tAEX zIh`&F{HBMD*qoO9GW6GkZL3yi;~ec5LTqnHsy&VsRIZ|ufa%j?Uqy$QY45tHQJ!*M4a3G3Hp!YfLY6uX3~GN_b}TStO~g+ppjc zzWRr@tm=RxJs!*N*-_2j-6`(hYl5rqbNk>swk>R&-|F%$Drd6_mygOl@X3BTZ;;iJ zF^sM^kt9MNR25$s$%|igmpLs_n5H^+X9H1-96`rAtLylYxLyE5yx~~|6x|{m*dmz5 zZl)qkVu9t*LH8%T4G2_y?J}HKk4%%e7P%WCgu#J}w6*-UWTNV^PCvG!gH%@`#*!$K zVNrnfh?aHzbb~vqKg8N49I_W}&`0gy)xoP2SK*hGvBDpY*2k@IKA$ve#5!{Uk8 zxe=U1ryCyv8#XIvzjkF!iw$f!TT4hb*7(*)oO0;}K(}k9MzDaXU~=@lQ^5Eg@u7$0 zUN97JV(1L-{GMh&v~vufgq>kXjqDV7?vKe#aPL>;qrwod`#gx3mBPx z%mpFP-@+J2$o>7c=P*(PdY!eu|6mCyNoIpA>A^lP<2^Tx_%}bqYgx$D7ZW=lN_KAb zu9B6BbvaG+qqS~5eCK3pb*X*mc@@lA0SsAj!-Y?>vjd#zVNoXsxG5c_q75!f3=_n> zPwulICGdStCpMofdWw_9wNh%NX*QOD#*k7-r*FxHGgKb=?LOE(r;QVH*Oi5QOok1@6_x91Y z>KB84T*gt8Ql$PlA>RYDejr%ee)!)3ZTk54lvJa;@VhekMp7~C-NhL5^uZko;%-we zmT1ED`_sfl=e*rm>Msw?U;2otuU?)KJvY|q3*<{NN?-wNzwq&4;zcl1aXdAx)c3@9 z01i&y#-C?f7+d%|Yq4i8b{?Wn-X4@AYQCz^IKG$=UaY1R44)JE*+RE$0(CBB3!%}g zO?5({f^1h_xIHvO5&Kn4IgfRisOJLslqo;>z6??Nz}mj+k*0$KjpXrq2(GeJ+QN!Ha>lv=3O+_ntn{t!$U7 zqZ8=Acsu&ZEXNQ@CK?&O-of0??)~RP-3!-*er3hSK5oN@yKNiyrTDj2Sx!u}W^SLY zqNl%?Tzo9}OZ?U97vVcNTddnUJLHRq7cB8`e~jy03J~0TRFZKJpOkmo-r_$vpSJZQ z2zttQJ@JK=maf}s=!Tg4sN$XMdHE?*z&rVZ;(*2KYWMxC>-K5ebx{uyv1$k}cf8;9 z0oHm^fo!xV(x<2>zSV+ymuVVQS_+6=VXb!3^4DDa&HbqSOl zL(AQDfsAt_L&>0Egbpb02BO>H1lu<3vW~G~5>al^E;@kij=~RPPT4rKg~iX75UH`_ zC5$)n$22tn~% zeuBHk92+FLMJCGrLRFG0itKq63fANW`TR{H={TS|`^iN3!x1cs7S+J{!AC?{Pqck` zeyAjb2x~q_;wk&uWL*bWZSbrJVIAJ)iq7im1!hPyN;6&cma#E?2a1-(9uhHmrjnZa zJ?|3+rs#}SmpJu#QB+-SHDJhFpg@`6lQS|m!3 z{*N+%D{QV&BI#e0h=T9BC=HyHF#?^0O~(SPML=W^#tc~7O4gPYwQlWVb{kLV(|^iY zT0hC54g6N%6&ml>q;Yeog`Bsr?q^UP&%eB5$GkOppBd@tD*@`$Q^sejb;}Ij!Mkrf zYu6Zv8?ea<+|P@-F$OUaK8yJRcf!9$N5p;mc^prG<+Erm5Pf1I;RGc)pgpZcNo=j< z=9CT#0GkUNc?(ZIU!UF~?#-XQ>a{LP$eXvKBjFD}E+#Ucb*P&FS`0uh$F_lwtha2? zV10|q{Mx;?HC?%#CQ1;F!EL_<_2#1R^Lq0t(K+`TnnlZen@${cgPF~*&|MqRbc9nS z{>>=HvS9KUV08eR%?SNJhsOWc2r&ahBs*P8Zr-xa8GnMm&o}7pQcdoC%Gm=L^|&SR zPw@WjO9SGxwS?4Kx>S{U0!@c8VTt6U=dHm{*Obd80|Qnm0>Xg-OJ^{z&zOJxomddC zI&rVx^)_xz-p45~y(NN923~6|s(}A1`*omc*U*mn?#BjA5x;HiYW^z%?sX?ju5=LZ#`5dDDETH-zh)qLO z@%en9DXfbFnepfb4(9JzygmBMxUcP1|7i_F2o`VyM^5DbVc}eV!~Nz+6hM$!*_(~Y zQ=l)|!})B+e;i2#1iS>lV7JYM7TruBLS&0%H8;Q)i32)#6cG{H^o^@g4wBc@@Yci` z4*8d5^Pj7_kJi*{o$10MJ~Rg5H!jfVA$$`?ps%jE(&=$RwQy;XLzzYC;QW<_7gFoyIU2*2m7hjAu21nf5nLO_{@cTS zNzWDr7+X2MhKjrzeoXRTDdAU^tW6Gpqr;%oO3ECQP|G+O;r9;0$CV06UrxXL%PICj z1A9_z-7l}M?+!)%xqJ_&Rz@+b$<9p_Ty5==1GvD@JD29#WNiVz*jc`#ZN2_5&i|<8 z+@qn+`#8RltUP6-Qf`$MxsB*13@N*6X{>uP8nTr8Z4G8Hz*Bqm|MWM*#j*qn6%R>> z)%0PC<9LgTR+}Mm&hL0*-=KTs|N;gY@j14f19(Q*yz(ua&TM*JT#)BL=wO3 zrvA9H#^oAUu0J^DN{w@+ujZQZvs_N2E5GnCe3%T!a7ajhTs_ijq-=McZqBDmcOk5G zkW!bt_(Sz3hMs{fg??shAi{}Li?9po-o1m%XSRtR4Mqn!i(w?u(w$A8>!vpIgL6!e zwRr%(EFc=_doI!WIj$4V;vv{~6%}pp_~=Daf+v_y2UiXq9ODGfhr;261_j%je;rkssy+|w0-YA zON@l<chzobs>zOdnj8=*ao=ie2E zKs78D>iUKr&B>A1Ys1C+pyD&wt0qRPwwCJpwu=gi;vT0xVm4bL3y2wRbTO{jl57b? zACm90HXM4C?POZ`T+M+KN%Pv|kh4sw)*7-<2_Gp_yMa;kb}=hx;uZ`Fv-o*Mu<5e5J<2 zI@~3}%NTk$12BQjd_>%!oLGIQL9r84rjtI$*4}6_Oq@s{r^~ntBIsdueVKL97KsAk zii{fP6HkF6hWd(D63=yYtvP|2ZEMWUYQi0>U|pH7b0_8{TMg%DMCbFQ7jC){8YE-) z%ouzt%Un0*1c{&e>*#)3PiXIqGcE407MPjo4abj3jY+4!&A8gctN}`8B%`4lI&q=X zA-ZbLmFYU+^llD6mJh*ZUAyD<^EdC43tT_mw{=^+Y$sL_hy+08Vb8{Q>`*7B|^ znTfscspnO%x>>Qum4;CP$`WiZ5nd^9Jx*u*nqT+C!O}^b+@av~7WlH*Xmxoqi;L?e zJ~zHDh!rgqa)ncsrnxYySA|G)dv-`uaHcO1S$^Tj3K>tq)MUz8YuRdx_iuwY3l3r% zwXUJ(CG6~Mr|4vj^8I9`UNP1V7$;kZ)&WJ`YPf+T)ojBXH^#q0zoAx|tw)#Eh#7`w zef8QuEGJXh>!&}h%k@D&``f;!%XV2AxC@?1+tKnGK>wb{FaKJUU-RO758JdShQra^ z48XYU+5{XdWUA+W;z_$mPt2+)2#oXi;J@C=odyqj*!)rmK^r;dU5C7LiCk({3P>SU)( z&DaZvK$C3-+nItV&02rC7p27H;3xkV#~r z)8-wmVjk@=SZ#*wNIrn799ApS*+hIHgJRHU(7@T{eN&BaGQ66@d~T>ecb_@s9G@Dg zj%eQjx(F>~8^_-5{DqVJl%hM)eiH!-<$H1~Z&P>7gZKk(_k zyg)6ajLj!i2Po7SL(PPV%i9O=DohN{a@VF8_}A5!FZ}7g45WFfIaq2b(7Ye|5_+JX zLv~8x4?r6*|Evw%HopAzh6e{VUR%rfF1}7SNELf$3WH?`2d2)*9mOzokQ^I1evYO* zjx?Ymw`U}EERYr<)#djh1AytGS0^q<@@_KyM_P*hdXMd0d|>{I4S5{7x3-b9I;e=@o5 zXSCNAZ_XL?uHg$0Jw%)oQyn@rz9wYP4d><+D=dzLOWo1c^rZfPxu#?_isi2Im*}m% zg}g^y$B^zUq2w=aqQ}LKa>}GbdLldT!@Ec+rguNMKONI`&OPWxr0b|^{LYOTbZioK zj9y3?f*gMIxM9O{y^bG-T&Jp#(bkFPw15muKFjN*y_N^ZdgCeCC;Z!z`j7ardK$Zc zB<8H&s3W!dY2Kz*-Q_p6W)jW;^XlXySufoO^HXQ#U%d-GFT=|S7Iyo<*WBGq39G#= zT~{@?#oL~ohv~4rnfm$d(0YZa6z)`}9P zzod3R+=G`O=2Ggpx@DjH_K-6K?!tEg+5miBgYW`u7&-*5W&rST@1WW7NZ@7y83v%G z|MX>)Lko=R7=sdhiPM2U<3VBSq6U4&FPTfa`j>npt*&oe;3u^`L@)nVsw~ctwhrr*!1hzv}S__L+^2zPROoiND zz)Gfvpb=v=6SMZ93!ozC%GnNmZiFJpq&TX=uZIhAEzLVWZ64H|ssphUNx*t_&*S*_ zYH{}<^deH8{_vY9KTvc%Q1Kg1cB zZ2WD-QV{nsZX>n9G`P-hh?E1Wo7t?$+R8!zNvs2T8rwitVOd;*&RDi_wFOEDA}ChM z8vhLVoUIGwgS)@8(_+v|X${5GJ(Hoe1FJz&3CQu#?@f5|>unSA`lD}^Qp-ZZt`_B3 zuq5;t%L7|9o5QCm;}qEjVt`UrrY`p+<(j?bWxjI54gq7X)quc@dywZ~v}0mUzu}=i zLi-O1BLd|&t`X%f*McF(p9oN6e{=fDza^_Um8a&1E*Lx*B{?*R+;jKH&rO=_D64LU z^c>AQh-JnK6b0jNDB+T%y?Q8O+Lc3E=I$=pJZag?jqS&LzW+F2yA+zDDSw=MUO?W- z#Ncm#>yL2z4|yo4|0NG)b8@=XZ!|a!<(Axu++dY<)~8ZFI_hq=qm{Xna5(_BPE0ml zG!e@W6+g~?n53;ZY2od>evM-!w23-R`9bRlS6%MgIAM_0zS6)uco_B$vW&eZUVvzvKuAr4JX=@92daPkqJme%O11(Q7Zl^NG=hI( zt_Z$7j+*6FFI^rTz90=|^0PlQzj$o^6ZbPW% V3=!7~Kpyp1hrUDEkoS8h{S(^M2WbER literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_refresh_24.xml b/app/src/main/res/drawable/ic_refresh_24.xml new file mode 100644 index 0000000..0f60427 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_24.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c014090..d571f24 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -94,4 +94,8 @@ Unable to open in any browser on your device + + View your failing pipelines + All Green + %s Red \ No newline at end of file diff --git a/app/src/main/res/xml/dashboard_widget_info.xml b/app/src/main/res/xml/dashboard_widget_info.xml new file mode 100644 index 0000000..aae6dd4 --- /dev/null +++ b/app/src/main/res/xml/dashboard_widget_info.xml @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/app/src/test/java/dev/aungkyawpaing/ccdroidx/feature/SyncProjectsTest.kt b/app/src/test/java/dev/aungkyawpaing/ccdroidx/feature/SyncProjectsTest.kt index 7cfb9ae..e91e9d9 100644 --- a/app/src/test/java/dev/aungkyawpaing/ccdroidx/feature/SyncProjectsTest.kt +++ b/app/src/test/java/dev/aungkyawpaing/ccdroidx/feature/SyncProjectsTest.kt @@ -2,14 +2,15 @@ package dev.aungkyawpaing.ccdroidx.feature import dev.aungkyawpaing.ccdroidx._testhelper_.CoroutineTest import dev.aungkyawpaing.ccdroidx._testhelper_.ProjectBuilder -import dev.aungkyawpaing.ccdroidx.data.api.NetworkException import dev.aungkyawpaing.ccdroidx.common.Project import dev.aungkyawpaing.ccdroidx.data.ProjectRepo +import dev.aungkyawpaing.ccdroidx.data.api.NetworkException import dev.aungkyawpaing.ccdroidx.feature.sync.LastSyncedState import dev.aungkyawpaing.ccdroidx.feature.sync.LastSyncedStatus import dev.aungkyawpaing.ccdroidx.feature.sync.SyncMetaDataStorage import dev.aungkyawpaing.ccdroidx.feature.sync.SyncProjects import dev.aungkyawpaing.ccdroidx.feature.wear.WearDataLayerSource +import dev.aungkyawpaing.ccdroidx.feature.widget.WidgetManager import io.mockk.coEvery import io.mockk.coVerify import io.mockk.coVerifyOrder @@ -30,6 +31,7 @@ class SyncProjectsTest : CoroutineTest() { private val clock: Clock = Clock.fixed(Instant.ofEpochSecond(6000), ZoneId.of("UTC")) private val syncMetaDataStorage = mockk(relaxed = true) private val wearDataLayerSource = mockk(relaxed = true) + private val widgetManager = mockk(relaxed = true) private val projectRepo = mockk(relaxUnitFun = true) private val syncProject = SyncProjects( @@ -37,6 +39,7 @@ class SyncProjectsTest : CoroutineTest() { syncMetaDataStorage, wearDataLayerSource, clock, + widgetManager ) @Test @@ -210,4 +213,23 @@ class SyncProjectsTest : CoroutineTest() { wearDataLayerSource.updateDataItems() } } + + @Test + fun `update widget manager on success`() = runTest { + val projectList = listOf(ProjectBuilder.buildProject()) + + coEvery { + projectRepo.getAll() + } returns flow { emit(projectList) } + + coEvery { + projectRepo.fetchRepo(any(), any(), any()) + } returns projectList + + syncProject.sync(mockk(relaxed = true)) + + coVerify(exactly = 1) { + widgetManager.updateDashboardWidget() + } + } } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 593987e..48e561c 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -83,7 +83,6 @@ dependencies { ksp(libs.moshi.codeGen) implementation(libs.dagger.hilt.android) - implementation(libs.dagger.hilt.work) ksp(libs.dagger.hilt.compiler) ksp(libs.dagger.hilt.android.compiler) androidTestImplementation(libs.dagger.hilt.android.testing) diff --git a/data/src/main/java/dev/aungkyawpaing/ccdroidx/data/ProjectRepo.kt b/data/src/main/java/dev/aungkyawpaing/ccdroidx/data/ProjectRepo.kt index 46b60e7..525ba5d 100644 --- a/data/src/main/java/dev/aungkyawpaing/ccdroidx/data/ProjectRepo.kt +++ b/data/src/main/java/dev/aungkyawpaing/ccdroidx/data/ProjectRepo.kt @@ -1,6 +1,7 @@ package dev.aungkyawpaing.ccdroidx.data import dev.aungkyawpaing.ccdroidx.common.Authentication +import dev.aungkyawpaing.ccdroidx.common.BuildStatus import dev.aungkyawpaing.ccdroidx.common.Project import dev.aungkyawpaing.ccdroidx.common.coroutine.DispatcherProvider import dev.aungkyawpaing.ccdroidx.data.api.FetchProject @@ -86,6 +87,12 @@ class ProjectRepo @Inject constructor( } } + fun getAllNotBuildStatus(buildStatus: BuildStatus): Flow> { + return projectTableDao.selectByNotBuildStatus(buildStatus) + .map { projectTables -> + projectTables.map(projectTableToProjectMapper::mapProjectTable) + } + } fun getById(projectId: Long): Project { return projectTableToProjectMapper.mapProjectTable(projectTableDao.selectById(projectId)) diff --git a/data/src/main/java/dev/aungkyawpaing/ccdroidx/data/db/ProjectTableDao.kt b/data/src/main/java/dev/aungkyawpaing/ccdroidx/data/db/ProjectTableDao.kt index 8a7b788..f38119f 100644 --- a/data/src/main/java/dev/aungkyawpaing/ccdroidx/data/db/ProjectTableDao.kt +++ b/data/src/main/java/dev/aungkyawpaing/ccdroidx/data/db/ProjectTableDao.kt @@ -54,6 +54,9 @@ interface ProjectTableDao { @Query("SELECT * FROM ProjectTable ORDER BY lastBuildStatus DESC, lastBuildTime DESC, id") fun selectAll(): Flow> + @Query("SELECT * FROM ProjectTable WHERE lastBuildStatus != :lastBuildStatus") + fun selectByNotBuildStatus(lastBuildStatus: BuildStatus): Flow> + @Query("SELECT * FROM ProjectTable WHERE id = :id") fun selectById(id: Long): ProjectTable diff --git a/data/src/test/java/dev/aungkyawpaing/ccdroidx/data/_testhelper_/FakeProjectTableDao.kt b/data/src/test/java/dev/aungkyawpaing/ccdroidx/data/_testhelper_/FakeProjectTableDao.kt index ff1985a..1e50dc6 100644 --- a/data/src/test/java/dev/aungkyawpaing/ccdroidx/data/_testhelper_/FakeProjectTableDao.kt +++ b/data/src/test/java/dev/aungkyawpaing/ccdroidx/data/_testhelper_/FakeProjectTableDao.kt @@ -83,6 +83,10 @@ class FakeProjectTableDao : ProjectTableDao { return flowOf(projectTables.values.toList()) } + override fun selectByNotBuildStatus(lastBuildStatus: BuildStatus): Flow> { + return flowOf(projectTables.values.filter { it.lastBuildStatus != lastBuildStatus }) + } + override fun selectById(id: Long): ProjectTable { return projectTables[idCounter]!! } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dd46e9c..4f0437d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,9 +10,11 @@ composeBom = "2024.03.00" compose-destinations = "1.10.2" compose-wear = "1.3.0" dagger = "2.51" +androidx-hilt = "1.2.0" desugar-jdk-libs = "2.0.4" espresso = "3.5.1" firebase-crashlytics-gradle = "2.9.9" +glance = "1.1.0-alpha01" google-services = "4.4.1" jupiter = "5.10.0" kotlin = "1.9.22" @@ -41,7 +43,9 @@ androidx-dataStore = { group = "androidx.datastore", name = "datastore", version androidx-dataStore-preference = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidx-dataStore" } androidx-fragment = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "androidx-fragment" } androidx-fragment-testing = { group = "androidx.fragment", name = "fragment-testing", version.ref = "androidx-fragment" } -androidx-hilt-navigation = { module = "androidx.hilt:hilt-navigation-compose", version = "1.2.0" } +androidx-glance-appwidget = { group = "androidx.glance", name = "glance-appwidget", version.ref = "glance" } +androidx-glance-appwidget-testing = { group = "androidx.glance", name = "glance-appwidget-testing", version.ref = "glance" } +androidx-glance-material3 = { group = "androidx.glance", name = "glance-material3", version.ref = "glance" } androidx-lifecycle-extensions = { group = "androidx.lifecycle", name = "lifecycle-extensions", version = "2.2.0" } androidx-lifecycle-java8 = { group = "androidx.lifecycle", name = "lifecycle-common-java8", version.ref = "androidx-lifecycle" } androidx-lifecycle-liveData = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "androidx-lifecycle" } @@ -84,14 +88,16 @@ coroutine-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutine coroutine-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutine" } coroutine-playServices = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "kotlinx-coroutine" } coroutine-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutine" } +androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "androidx-hilt" } +androidx-hilt-navigation = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidx-hilt" } +androidx-hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "androidx-hilt" } dagger-hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "dagger" } dagger-hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "dagger" } dagger-hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "dagger" } dagger-hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "dagger" } -dagger-hilt-work = { module = "androidx.hilt:hilt-work", version = "1.2.0" } desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar-jdk-libs" } firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" } -firebase-bom = "com.google.firebase:firebase-bom:32.7.4" +firebase-bom = "com.google.firebase:firebase-bom:32.8.0" firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" } junit-junit4 = { module = "junit:junit", version = "4.13.2" } junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "jupiter" } diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 6edf8f7..bd9503d 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -149,7 +149,6 @@ dependencies { implementation(libs.coroutine.playServices) implementation(libs.dagger.hilt.android) - implementation(libs.dagger.hilt.work) ksp(libs.dagger.hilt.compiler) ksp(libs.dagger.hilt.android.compiler) androidTestImplementation(libs.dagger.hilt.android.testing) diff --git a/weardatalayer/build.gradle.kts b/weardatalayer/build.gradle.kts index 79562fc..c252de7 100644 --- a/weardatalayer/build.gradle.kts +++ b/weardatalayer/build.gradle.kts @@ -50,7 +50,6 @@ dependencies { ksp(libs.moshi.codeGen) implementation(libs.dagger.hilt.android) - implementation(libs.dagger.hilt.work) ksp(libs.dagger.hilt.compiler) ksp(libs.dagger.hilt.android.compiler) androidTestImplementation(libs.dagger.hilt.android.testing)