Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package me.nya_n.notificationnotifier

import android.app.Application
import me.nya_n.notificationnotifier.data.repository.AppRepository
import me.nya_n.notificationnotifier.data.repository.BackupRepository
import me.nya_n.notificationnotifier.data.repository.UserSettingsRepository
import me.nya_n.notificationnotifier.data.repository.impl.AppRepositoryImpl
import me.nya_n.notificationnotifier.data.repository.impl.BackupRepositoryImpl
import me.nya_n.notificationnotifier.data.repository.impl.UserSettingsRepositoryImpl
import me.nya_n.notificationnotifier.data.repository.source.DB
import me.nya_n.notificationnotifier.data.repository.source.UserSettingsDataStore
Expand Down Expand Up @@ -81,20 +83,21 @@ class App : Application() {

// Repository
single<UserSettingsRepository> { UserSettingsRepositoryImpl(get()) }
single<AppRepository> { AppRepositoryImpl(get(), get()) }
single<AppRepository> { AppRepositoryImpl(applicationContext.packageManager, get(), get()) }
single<BackupRepository> { BackupRepositoryImpl(applicationContext) }

// ViewModel
viewModel { AppViewModel(get(), packageName, get(), get()) }
viewModel { SelectionViewModel(get(), get(), get()) }
viewModel { SelectionViewModel(get(), get()) }
viewModel { params -> DetailViewModel(get(), get(), get(), get(), params.get()) }
viewModel { TargetViewModel(get(), get()) }
viewModel { TargetViewModel(get()) }
viewModel { SettingsViewModel(get(), get(), get(), get(), get(), get()) }

// UseCase
factory<AddTargetAppUseCase> { AddTargetAppUseCaseImpl(get()) }
factory<DeleteTargetAppUseCase> { DeleteTargetAppUseCaseImpl(get()) }
factory<ExportDataUseCase> { ExportDataUseCaseImpl(get(), get()) }
factory<ImportDataUseCase> { ImportDataUseCaseImpl(get(), get()) }
factory<ExportDataUseCase> { ExportDataUseCaseImpl(get(), get(), get()) }
factory<ImportDataUseCase> { ImportDataUseCaseImpl(get(), get(), get()) }
factory<LoadAddressUseCase> { LoadAddressUseCaseImpl(get()) }
factory<LoadAppUseCase> { LoadAppUseCaseImpl(get(), get()) }
factory<LoadFilterConditionUseCase> { LoadFilterConditionUseCaseImpl(get()) }
Expand Down
6 changes: 6 additions & 0 deletions AndroidApp/data/repository/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ dependencies {
// room
api(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)

// test
androidTestImplementation(libs.junit)
androidTestImplementation(libs.com.google.truth)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.kotlinx.coroutines.test)
}

ksp {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package me.nya_n.notificationnotifier

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import me.nya_n.notificationnotifier.data.repository.AppRepository
import me.nya_n.notificationnotifier.data.repository.impl.AppRepositoryImpl
import me.nya_n.notificationnotifier.data.repository.source.DB
import me.nya_n.notificationnotifier.model.FilterCondition
import me.nya_n.notificationnotifier.model.InstalledApp
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@Suppress("NonAsciiCharacters")
@RunWith(AndroidJUnit4::class)
class AppRepositoryTest {
@OptIn(ExperimentalCoroutinesApi::class)
private val testDispatcher = UnconfinedTestDispatcher()
private lateinit var appRepository: AppRepository

@Before
fun setUp() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val db = DB.get(appContext, isInMemory = true).apply {
clearAllTables()
}
appRepository = AppRepositoryImpl(
appContext.packageManager,
db.filterConditionDao(),
db.targetAppDao(),
testDispatcher
)
}

@Test
fun `通知対象アプリの追加、取得、削除`() {
runTest(testDispatcher) {
val app = InstalledApp("sample", "com.sample.www")
appRepository.addTargetApp(app)

val added = appRepository.getTargetAppList()
assertThat(added).hasSize(1)
assertThat(added.first()).isEqualTo(app)

appRepository.deleteTargetApp(app)
val deleted = appRepository.getTargetAppList()
assertThat(deleted).isEmpty()
}
}


@Test
fun `通知条件の追加、取得、更新`() {
runTest(testDispatcher) {
val packageName = "com.sample.www"

// データなし
assertThat(appRepository.getFilterCondition(packageName)).isNull()
assertThat(appRepository.getFilterConditionOrDefault(packageName))
.isEqualTo(FilterCondition.default(packageName))

// 追加
val added = FilterCondition(packageName, false, "test")
appRepository.saveFilterCondition(added)
assertThat(appRepository.getFilterCondition(packageName)).isEqualTo(added)

// メッセージ条件の更新
val updatedCondition = added.copy(condition = "updated")
appRepository.saveFilterCondition(updatedCondition)
assertThat(appRepository.getFilterCondition(packageName)).isEqualTo(updatedCondition)

// サマリー条件の更新
val updatedSummary = updatedCondition.copy(isIgnoreSummary = true)
appRepository.saveFilterCondition(updatedSummary)
assertThat(appRepository.getFilterCondition(packageName)).isEqualTo(updatedSummary)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package me.nya_n.notificationnotifier

import androidx.core.content.edit
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import me.nya_n.notificationnotifier.data.repository.UserSettingsRepository
import me.nya_n.notificationnotifier.data.repository.impl.UserSettingsRepositoryImpl
import me.nya_n.notificationnotifier.data.repository.source.UserSettingsDataStore
import me.nya_n.notificationnotifier.data.repository.util.SharedPreferenceProvider
import me.nya_n.notificationnotifier.model.UserSettings
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@Suppress("NonAsciiCharacters")
@RunWith(AndroidJUnit4::class)
class UserSettingsRepositoryTest {
@OptIn(ExperimentalCoroutinesApi::class)
private val testDispatcher = UnconfinedTestDispatcher()
private lateinit var userSettingsRepository: UserSettingsRepository

@Before
fun setUp() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
userSettingsRepository = UserSettingsRepositoryImpl(
UserSettingsDataStore(
SharedPreferenceProvider.create(
appContext,
UserSettingsDataStore.DATA_STORE_NAME
).apply {
edit {
clear()
}
}
)
)
}

@Test
fun `ユーザー設定の保存、取得、更新`() {
runTest(testDispatcher) {
val data = UserSettings("192.168.10.18", 8484, false)
userSettingsRepository.saveUserSettings(data)
assertThat(userSettingsRepository.getUserSettings()).isEqualTo(data)

val updated = data.copy(port = 2525, isPackageVisibilityGranted = true)
userSettingsRepository.saveUserSettings(updated)
assertThat(userSettingsRepository.getUserSettings()).isEqualTo(updated)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package me.nya_n.notificationnotifier.data.repository

import android.content.pm.PackageManager
import me.nya_n.notificationnotifier.model.FilterCondition
import me.nya_n.notificationnotifier.model.InstalledApp

Expand All @@ -23,5 +22,5 @@ interface AppRepository {

suspend fun deleteTargetApp(target: InstalledApp)

fun loadInstalledAppList(pm: PackageManager): List<InstalledApp>
fun loadInstalledAppList(): List<InstalledApp>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package me.nya_n.notificationnotifier.data.repository

import android.net.Uri

interface BackupRepository {
/** [uri]に通知対象や条件、設定を保存
* @param uri 保存先
* @param data 保存するデータ
*/
suspend fun exportToUri(uri: Uri, data: String)

/** [uri]から通知対象や条件、設定を復元
* @param uri 読み込み元
* @return 復元したデータ
*/
suspend fun importFromUri(uri: Uri): String
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import me.nya_n.notificationnotifier.model.FilterCondition
import me.nya_n.notificationnotifier.model.InstalledApp

class AppRepositoryImpl(
private val packageManager: PackageManager,
private val filterConditionDao: FilterConditionDao,
private val targetAppDao: TargetAppDao,
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO
Expand Down Expand Up @@ -62,10 +63,10 @@ class AppRepositoryImpl(
}
}

override fun loadInstalledAppList(pm: PackageManager): List<InstalledApp> {
return pm.getInstalledApplications(PackageManager.GET_META_DATA)
override fun loadInstalledAppList(): List<InstalledApp> {
return packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
.map {
val label = pm.getApplicationLabel(it).toString()
val label = packageManager.getApplicationLabel(it).toString()
InstalledApp(
label,
it.packageName
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package me.nya_n.notificationnotifier.data.repository.impl

import android.content.Context
import android.net.Uri
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import me.nya_n.notificationnotifier.data.repository.BackupRepository
import java.io.BufferedReader
import java.io.InputStreamReader

class BackupRepositoryImpl(
private val context: Context,
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO
) : BackupRepository {
override suspend fun exportToUri(uri: Uri, data: String) {
withContext(coroutineDispatcher) {
context.contentResolver.openOutputStream(uri)?.use {
it.write(data.toByteArray())
} ?: throw RuntimeException("Failed to open output stream.")
}
}

override suspend fun importFromUri(uri: Uri): String {
val sb = StringBuilder()
withContext(coroutineDispatcher) {
context.contentResolver.openInputStream(uri)?.use { input ->
BufferedReader(InputStreamReader(input)).use { reader ->
sb.append(reader.readLine())
} ?: throw RuntimeException("Failed to read input stream.")
} ?: throw RuntimeException("Failed to open input stream.")
}
return sb.toString()
}
Comment on lines +24 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

importFromUriの実装に問題があります

2つの問題があります:

  1. Line 30のnullチェックが機能しない: BufferedReader.use { } は常にラムダの結果(sb.append()の戻り値であるStringBuilder)を返すため、?: throw RuntimeException("Failed to read input stream.") は決して実行されません。

  2. readLine()がnullを返す場合の処理: ストリームが空の場合、reader.readLine()はnullを返し、sb.append(null)は文字列"null"を追加してしまいます。

🐛 修正案
 override suspend fun importFromUri(uri: Uri): String {
-    val sb = StringBuilder()
     withContext(coroutineDispatcher) {
         context.contentResolver.openInputStream(uri)?.use { input ->
             BufferedReader(InputStreamReader(input)).use { reader ->
-                sb.append(reader.readLine())
-            } ?: throw RuntimeException("Failed to read input stream.")
+                return@withContext reader.readLine()
+                    ?: throw RuntimeException("Failed to read input stream.")
+            }
         } ?: throw RuntimeException("Failed to open input stream.")
     }
-    return sb.toString()
 }
🤖 Prompt for AI Agents
In
`@AndroidApp/data/repository/src/main/kotlin/me/nya_n/notificationnotifier/data/repository/impl/BackupRepositoryImpl.kt`
around lines 24 - 34, importFromUri incorrectly relies on a post-`use` Elvis and
appends a possible null line; change it to (inside
BackupRepositoryImpl.importFromUri) open the input stream with
context.contentResolver.openInputStream(uri) and throw if null, then use
BufferedReader(InputStreamReader(input)).use { reader -> val text =
reader.readText(); if (text.isEmpty()) throw RuntimeException("Empty input
stream") else return text } so you don't depend on reader.readLine() returning
non-null and you remove the ineffective `?:` after `use`.

}
6 changes: 6 additions & 0 deletions AndroidApp/domain/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@ dependencies {
api(libs.androidx.compose.runtime)

// test
testImplementation(libs.junit)
testImplementation(libs.com.google.truth)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.com.google.truth)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.espresso.core)
androidTestImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.mockk)
androidTestImplementation(libs.mockk.android)

// その他
implementation(libs.com.google.code.gson)
Expand Down
Loading