Skip to content

Commit d0730cf

Browse files
authored
Merge pull request #100 from figonzal1/dev
fix: cleartext traffic, share cache errors and Sonar/Kover coverage
2 parents 38f5b29 + 4d1b0a0 commit d0730cf

11 files changed

Lines changed: 80 additions & 78 deletions

File tree

CLAUDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ bundle exec fastlane unit_test # unit tests
4444
bundle exec fastlane ui_test # instrumentation tests (DevDebug)
4545
bundle exec fastlane release # build the prodRelease AAB (used for every track)
4646
bundle exec fastlane beta_googleplay # upload that AAB to the beta track
47+
48+
# SonarQube (local self-hosted server on :9000):
49+
./gradlew koverXmlReportDevDebug sonar # coverage (Kover) + analysis
4750
```
4851

4952
## Gotchas
@@ -67,6 +70,13 @@ bundle exec fastlane beta_googleplay # upload that AAB to the beta track
6770
not from the flavor. `devDebug` is `cl.figonzal.lastquakechile.debug` and both package names are
6871
registered in `app/google-services.json` — moving that suffix onto the `dev` flavor would leave
6972
`prodDebug` holding the production applicationId.
73+
- SonarQube runs **locally only**, never in CI. `sonar.host.url` lives in
74+
`~/.gradle/gradle.properties` and the token in the `SONAR_TOKEN` env var — never in the repo.
75+
Coverage comes from **Kover** (`koverXmlReportDevDebug`
76+
`app/build/reports/kover/report-devDebug.xml`), not AGP's JaCoCo: on AGP 9 the
77+
`createDevDebugUnitTestCoverageReport` task only emits HTML and Sonar needs XML.
78+
- The `org.sonarqube` plugin must be **>= 7.4**; 7.3.0.8198 blows up on AGP 9.2 — its
79+
`sonarResolver` task reads the `res` directories before `generateDevDebugResValues` runs.
7080
- Dependencies live in `gradle/libs.versions.toml`; `app/build.gradle.kts` only references
7181
`libs.*` aliases. Never add a raw coordinate to the build file.
7282
- Instrumentation tests run through `InstrumentationTestRunner` + `TestApplication`, with Koin

app/build.gradle.kts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ plugins {
1515
alias(libs.plugins.com.google.firebase.firebase.perf)
1616

1717
alias(libs.plugins.com.google.android.libraries.mapsplatform.secrets.gradle.plugin)
18+
19+
alias(libs.plugins.org.jetbrains.kotlinx.kover)
1820
}
1921

2022
secrets {
@@ -213,12 +215,24 @@ ksp {
213215
arg("room.schemaLocation", "$projectDir/schemas")
214216
}
215217

216-
sonarqube {
218+
kover {
219+
reports {
220+
filters { excludes { androidGeneratedClasses() } }
221+
variant("devDebug") {
222+
xml { xmlFile = layout.buildDirectory.file("reports/kover/report-devDebug.xml") }
223+
}
224+
}
225+
}
226+
227+
sonar {
217228
properties {
218-
property("sonar.projectName", "LastQuakeChile")
219229
property("sonar.projectKey", "LastQuakeChile")
220-
property("sonar.test.inclusions", "**/*Test*/**")
230+
property("sonar.projectName", "LastQuakeChile")
231+
property("sonar.projectVersion", appVersionName)
221232
property("sonar.sourceEncoding", "UTF-8")
222-
property("sonar.sources", "src/main/java")
233+
property(
234+
"sonar.coverage.jacoco.xmlReportPaths",
235+
layout.buildDirectory.file("reports/kover/report-devDebug.xml").get().asFile.path
236+
)
223237
}
224238
}

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
android:roundIcon="@mipmap/ic_launcher_round"
2727
android:supportsRtl="true"
2828
android:theme="@style/AppTheme.Starting"
29+
android:usesCleartextTraffic="false"
2930
tools:targetApi="n">
3031

3132
<uses-library
@@ -103,10 +104,10 @@
103104
<meta-data
104105
android:name="android.support.FILE_PROVIDER_PATHS"
105106
android:resource="@xml/file_provider" />
106-
</provider> <!-- TODO: Habilitar para produccion -->
107+
</provider>
107108
<meta-data
108109
android:name="firebase_performance_collection_enabled"
109-
android:value="true" /> <!-- TODO: Habilitar para produccion -->
110+
android:value="true" />
110111
<meta-data
111112
android:name="firebase_crashlytics_collection_enabled"
112113
android:value="true" />

app/src/main/java/cl/figonzal/lastquakechile/core/ui/MainActivity.kt

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidx.core.view.ViewCompat
1212
import androidx.core.view.WindowInsetsCompat
1313
import androidx.core.view.doOnLayout
1414
import androidx.core.view.isVisible
15+
import androidx.lifecycle.lifecycleScope
1516
import androidx.preference.PreferenceManager
1617
import cl.figonzal.lastquakechile.R
1718
import cl.figonzal.lastquakechile.core.services.UpdaterService
@@ -30,8 +31,6 @@ import com.google.android.material.tabs.TabLayout
3031
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
3132
import com.google.android.material.tabs.TabLayoutMediator
3233
import kotlinx.coroutines.CoroutineDispatcher
33-
import kotlinx.coroutines.CoroutineScope
34-
import kotlinx.coroutines.Dispatchers
3534
import kotlinx.coroutines.launch
3635
import kotlinx.coroutines.withContext
3736
import org.koin.android.ext.android.inject
@@ -84,16 +83,9 @@ class MainActivity : AppCompatActivity() {
8483
initLifecycleObservers()
8584

8685
checkEULAConsentAds {
87-
88-
val adsScope = CoroutineScope(ioDispatcher)
89-
adsScope.launch {
90-
91-
MobileAds.initialize(this@MainActivity)
92-
93-
withContext(Dispatchers.Main) {
94-
//Ads
95-
adView = startAds(binding.adViewContainer)
96-
}
86+
lifecycleScope.launch {
87+
withContext(ioDispatcher) { MobileAds.initialize(this@MainActivity) }
88+
adView = startAds(binding.adViewContainer)
9789
}
9890
}
9991

app/src/main/java/cl/figonzal/lastquakechile/core/utils/BitmapExt.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import android.graphics.Color
88
import android.net.Uri
99
import android.view.View
1010
import androidx.core.content.FileProvider
11+
import timber.log.Timber
1112
import java.io.File
1213
import java.io.FileOutputStream
1314
import java.io.IOException
@@ -54,7 +55,9 @@ fun View.renderToBitmap(widthPx: Int): Bitmap {
5455
* a single share can now produce several images (one per [StickerDesign]) that must coexist.
5556
*/
5657
fun Context.clearShareImageCache() {
57-
shareCacheDir.listFiles()?.forEach { it.delete() }
58+
shareCacheDir.listFiles()?.forEach {
59+
if (!it.delete()) Timber.w("Could not delete cached share image %s", it.name)
60+
}
5861
}
5962

6063
/**

app/src/main/java/cl/figonzal/lastquakechile/core/utils/ShareExt.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import cl.figonzal.lastquakechile.quake_feature.domain.model.Quake
1515
internal const val INSTAGRAM_PACKAGE = "com.instagram.android"
1616
private const val INSTAGRAM_STORY_ACTION = "com.instagram.share.ADD_TO_STORY"
1717
internal const val WHATSAPP_PACKAGE = "com.whatsapp"
18+
private const val MIME_IMAGE = "image/*"
1819

1920
fun Context.buildShareText(quake: Quake): String = String.format(
2021
"""
@@ -47,13 +48,13 @@ fun Context.buildShareText(quake: Quake): String = String.format(
4748
*/
4849
private fun instagramStoryIntent(imageUri: Uri?): Intent = Intent(INSTAGRAM_STORY_ACTION).apply {
4950
setPackage(INSTAGRAM_PACKAGE)
50-
type = "image/*"
51+
type = MIME_IMAGE
5152
imageUri?.let { putExtra("interactive_asset_uri", it) }
5253
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
5354
}
5455

5556
private fun whatsAppSendIntent(): Intent =
56-
Intent(Intent.ACTION_SEND).setPackage(WHATSAPP_PACKAGE).setType("image/*")
57+
Intent(Intent.ACTION_SEND).setPackage(WHATSAPP_PACKAGE).setType(MIME_IMAGE)
5758

5859
fun Context.isInstagramStoriesAvailable(): Boolean =
5960
resolveActivityOrNull(instagramStoryIntent(null)) != null
@@ -117,25 +118,24 @@ fun Context.shareQuakeGeneric(quake: Quake, imageUri: Uri?) {
117118
action = Intent.ACTION_SEND
118119
putExtra(Intent.EXTRA_TEXT, buildShareText(quake))
119120
putExtra(Intent.EXTRA_STREAM, imageUri)
120-
type = "image/*"
121+
type = MIME_IMAGE
121122

122123
val chooser = Intent.createChooser(this, getString(R.string.intent_chooser))
123124

124-
val resInfoList = when {
125+
val targetPackages = when {
125126
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> packageManager.queryIntentActivities(
126127
chooser,
127128
ResolveInfoFlags.of(MATCH_DEFAULT_ONLY.toLong())
128129
)
129130

130131
else -> packageManager.queryIntentActivities(chooser, MATCH_DEFAULT_ONLY)
131-
}
132+
}.map { it.activityInfo.packageName }
132133

133-
for (resolveInfo in resInfoList) {
134-
val packageName = resolveInfo.activityInfo.packageName
135-
imageUri?.let {
134+
imageUri?.let { uri ->
135+
targetPackages.forEach { packageName ->
136136
grantUriPermission(
137137
packageName,
138-
it,
138+
uri,
139139
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
140140
)
141141
}

app/src/main/java/cl/figonzal/lastquakechile/core/utils/views/ViewsExt.kt

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ import android.view.MenuInflater
1313
import android.view.MenuItem
1414
import android.view.View
1515
import android.view.ViewGroup
16-
import android.view.ViewTreeObserver
1716
import android.widget.AbsListView
1817
import android.widget.ImageView
1918
import android.widget.TextView
2019
import android.widget.Toast
2120
import androidx.annotation.MenuRes
2221
import androidx.core.view.MenuHost
2322
import androidx.core.view.MenuProvider
23+
import androidx.core.view.doOnNextLayout
2424
import androidx.fragment.app.Fragment
2525
import androidx.lifecycle.Lifecycle
2626
import androidx.recyclerview.widget.LinearLayoutManager
@@ -472,16 +472,7 @@ fun RecyclerView.addPaginationListener(
472472
fun ViewGroup.getViewBottomHeight(
473473
targetViewId: Int,
474474
behavior: BottomSheetBehavior<MaterialCardView>?
475-
) {
476-
477-
val callback = object : ViewTreeObserver.OnGlobalLayoutListener {
478-
override fun onGlobalLayout() {
479-
viewTreeObserver.removeOnGlobalLayoutListener(this)
480-
behavior?.peekHeight =
481-
findViewById<View>(targetViewId).bottom + 20f.toDips(resources).toInt()
482-
}
483-
}
484-
485-
viewTreeObserver.addOnGlobalLayoutListener(callback)
475+
) = doOnNextLayout {
476+
behavior?.peekHeight = findViewById<View>(targetViewId).bottom + 20f.toDips(resources).toInt()
486477
}
487478

app/src/main/java/cl/figonzal/lastquakechile/quake_feature/ui/QuakeDetailsActivity.kt

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,12 @@ import com.google.android.gms.maps.model.LatLng
5959
import com.google.maps.android.ktx.addCircle
6060
import kotlinx.coroutines.CancellationException
6161
import kotlinx.coroutines.CompletableDeferred
62-
import kotlinx.coroutines.Dispatchers
62+
import kotlinx.coroutines.CoroutineDispatcher
6363
import kotlinx.coroutines.launch
6464
import kotlinx.coroutines.withContext
6565
import kotlinx.coroutines.withTimeoutOrNull
6666
import org.koin.android.ext.android.inject
67+
import org.koin.core.qualifier.named
6768
import timber.log.Timber
6869
import java.util.Locale
6970

@@ -121,6 +122,7 @@ class QuakeDetailsActivity : AppCompatActivity(), OnMapReadyCallback {
121122
private var isSnapshotRequest: Boolean? = null
122123

123124
private val quakeStoryRenderer: QuakeStoryRenderer by inject()
125+
private val ioDispatcher: CoroutineDispatcher by inject(named("ioDispatcher"))
124126

125127
private lateinit var binding: ActivityQuakeDetailsBinding
126128

@@ -178,11 +180,9 @@ class QuakeDetailsActivity : AppCompatActivity(), OnMapReadyCallback {
178180
}
179181

180182
private fun refreshAd() {
181-
lifecycleScope.launch(Dispatchers.IO) {
182-
MobileAds.initialize(this@QuakeDetailsActivity)
183-
withContext(Dispatchers.Main) {
184-
loadNativeAd()
185-
}
183+
lifecycleScope.launch {
184+
withContext(ioDispatcher) { MobileAds.initialize(this@QuakeDetailsActivity) }
185+
loadNativeAd()
186186
}
187187
}
188188

@@ -386,36 +386,36 @@ class QuakeDetailsActivity : AppCompatActivity(), OnMapReadyCallback {
386386

387387
/**
388388
* Renders all [StickerDesign] variants (map snapshot may be null if it couldn't be
389-
* captured) and opens [ShareQuakeBottomSheet] with them. Rendering happens off the main
390-
* thread: the sticker views are never attached to a window, so it's safe to
391-
* measure/layout/draw them from any thread, and PNG compression to the cache dir is I/O.
392-
* Designs are rendered sequentially and each bitmap is recycled right after it's cached -
393-
* doing all three at once would keep ~18 MB of bitmaps alive simultaneously.
389+
* captured) and opens [ShareQuakeBottomSheet] with them. Rendering happens on [ioDispatcher]:
390+
* the sticker views are never attached to a window, so it's safe to measure/layout/draw them
391+
* off the main thread, and PNG compression to the cache dir is I/O. Designs are rendered
392+
* sequentially and each bitmap is recycled right after it's cached - doing all three at once
393+
* would keep ~18 MB of bitmaps alive simultaneously.
394394
*/
395395
private fun shareQuake(quake: Quake) {
396396
captureMapSnapshot { mapSnapshot ->
397-
lifecycleScope.launch(Dispatchers.Default) {
397+
lifecycleScope.launch {
398398
try {
399-
clearShareImageCache()
400-
401-
val stickerUris = StickerDesign.entries.map { design ->
402-
val sticker = quakeStoryRenderer.renderSticker(quake, mapSnapshot, design)
403-
cacheImageUri(sticker, "sticker-${quake.quakeCode}-${design.name}", Bitmap.CompressFormat.PNG)
404-
.also { sticker.recycle() }
399+
val stickerUris = withContext(ioDispatcher) {
400+
clearShareImageCache()
401+
402+
StickerDesign.entries.map { design ->
403+
val sticker = quakeStoryRenderer.renderSticker(quake, mapSnapshot, design)
404+
cacheImageUri(sticker, "sticker-${quake.quakeCode}-${design.name}", Bitmap.CompressFormat.PNG)
405+
.also { sticker.recycle() }
406+
}
405407
}
406-
val magnitudeColor = quakeStoryRenderer.magnitudeColor(quake)
407408

408-
withContext(Dispatchers.Main) {
409-
if (supportFragmentManager.isStateSaved) return@withContext
409+
if (supportFragmentManager.isStateSaved) return@launch
410410

411-
ShareQuakeBottomSheet.newInstance(quake, stickerUris, magnitudeColor)
412-
.show(supportFragmentManager, ShareQuakeBottomSheet.TAG)
413-
}
411+
ShareQuakeBottomSheet
412+
.newInstance(quake, stickerUris, quakeStoryRenderer.magnitudeColor(quake))
413+
.show(supportFragmentManager, ShareQuakeBottomSheet.TAG)
414414
} catch (e: CancellationException) {
415415
throw e
416416
} catch (e: Exception) {
417417
Timber.e(e, "Share image generation failed")
418-
withContext(Dispatchers.Main) { toast(R.string.SHARE_ERROR) }
418+
toast(R.string.SHARE_ERROR)
419419
}
420420
}
421421
}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<paths>
3-
<!-- See table below. The external-files-path DOES NOT require external storage permissions. -->
43
<cache-path
54
name="images"
65
path="share/" />
7-
8-
<!--Uncomment below to share the entire application specific directory -->
9-
<!--<external-path name="all_dirs" path="."/>-->
10-
</paths>
6+
</paths>
Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<network-security-config>
3-
<!-- TODO: false en produccion, true con appodeal -->
43
<base-config cleartextTrafficPermitted="false">
54
<trust-anchors>
65
<certificates src="system" />
76
</trust-anchors>
87
</base-config>
9-
10-
<domain-config cleartextTrafficPermitted="true">
11-
<domain includeSubdomains="true">127.0.0.1</domain>
12-
</domain-config>
13-
14-
</network-security-config>
8+
</network-security-config>

0 commit comments

Comments
 (0)