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
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ android {
applicationId = "com.doyoonkim.knutice"
minSdk = 31
targetSdk = 34
versionCode = 13
versionName = "1.3.3"
versionCode = 15
versionName = "1.4.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.KNUTICE"
android:windowSoftInputMode="adjustResize"
tools:targetApi="31">

<meta-data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import com.doyoonkim.knutice.model.ApiDeviceTokenRequest
import com.doyoonkim.knutice.model.DeviceTokenRequest
import com.doyoonkim.knutice.model.NoticeCategory
import com.doyoonkim.knutice.model.NoticesPerPage
import com.doyoonkim.knutice.model.TopThreeNotices
import com.doyoonkim.knutice.model.ApiPostResult
import com.doyoonkim.knutice.BuildConfig
import com.doyoonkim.knutice.model.ApiReportRequest
import com.doyoonkim.knutice.model.ApiTopicSubscriptionRequest
import com.doyoonkim.knutice.model.ManageTopicRequest
import com.doyoonkim.knutice.model.SingleNotice
import com.doyoonkim.knutice.model.ReportRequest
import com.doyoonkim.knutice.model.TopicSubscriptionStatusDTO
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
Expand All @@ -21,8 +22,10 @@ import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
import javax.inject.Inject
import javax.inject.Singleton
Expand Down Expand Up @@ -55,11 +58,25 @@ class KnuticeRemoteSource @Inject constructor() {
}
}

suspend fun getNoticeById(id: String): Result<SingleNotice> {
return runCatching {
knuticeService.create(KnuticeService::class.java).run {
this.getSingleNoticeById(id)
}
}.onFailure { throw it }
}

suspend fun queryNoticesByKeyword(keyword: String): NoticesPerPage {
Log.d("KnuticeRemoteSource", "Start retrofit service (Querying Notices...)")
return knuticeService.create(KnuticeService::class.java).queryNoticeByKeyword(keyword)
}

suspend fun getTopicSubscriptionStatus(): Result<TopicSubscriptionStatusDTO> {
return runCatching {
knuticeService.create(KnuticeService::class.java).getTopicSubscriptionStatus(validatedToken)
}.onFailure { throw it }
}

suspend fun getFullNoticeContent(url: String): Deferred<String> =
CoroutineScope(Dispatchers.IO).async {
Jsoup.connect(url)
Expand Down Expand Up @@ -135,44 +152,53 @@ class KnuticeRemoteSource @Inject constructor() {

interface KnuticeService {

@GET("/open-api/notice/list")
@GET("open-api/notice/list")
suspend fun getTopThreeNotice(
@Query("noticeName") category: NoticeCategory,
@Query("size") size: Int
): NoticesPerPage

@GET("/open-api/notice/list")
@GET("open-api/notice/list")
suspend fun getNoticeListPerPage(
@Query("noticeName") category: NoticeCategory,
@Query("nttId") lastNttId: Int
): NoticesPerPage

@GET("/open-api/notice/list")
@GET("open-api/notice/list")
suspend fun getFirstPageOfNotice(
@Query("noticeName") category: NoticeCategory
): NoticesPerPage

@GET("/open-api/search")
@GET("open-api/notice/{nttId}")
suspend fun getSingleNoticeById(
@Path("nttId") nttId: String
): SingleNotice

@GET("open-api/search")
suspend fun queryNoticeByKeyword(
@Query("keyword") keyword: String
): NoticesPerPage

@Headers("Content-Type: application/json")
@POST("/open-api/fcm")
@POST("open-api/fcm")
suspend fun validateToken(
@Body requestBody: ApiDeviceTokenRequest
): ApiPostResult

@Headers("Content-Type: application/json")
@POST("/open-api/report")
@POST("open-api/report")
suspend fun submitUserReport(
@Body requestBody: ApiReportRequest
): ApiPostResult

@Headers("Content-Type: application/json")
@POST("/open-api/topic")
@POST("open-api/topic")
suspend fun submitTopicSubscriptionPreference(
@Body requestBody: ApiTopicSubscriptionRequest
): ApiPostResult

@GET("open-api/topic")
suspend fun getTopicSubscriptionStatus(
@Header("fcmToken") token: String
): TopicSubscriptionStatusDTO
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.doyoonkim.knutice.domain

import android.util.Log
import com.doyoonkim.knutice.data.KnuticeRemoteSource
import com.doyoonkim.knutice.model.Notice
import javax.inject.Inject


interface fetchSingleNotice {
suspend fun getSingleNoticeById(nttId: String): Notice
}

class FetchSingleNoticeImpl @Inject constructor(
private val remoteSource: KnuticeRemoteSource
) : fetchSingleNotice {

override suspend fun getSingleNoticeById(nttId: String): Notice {
remoteSource.getNoticeById(nttId).fold(
onSuccess = {
Log.d("FetchSingleNoticeImpl", it.notice?.toNotice().toString())
return it.notice?.toNotice() ?: Notice()
},
onFailure = {
Log.d("FetchSingleNoticeImpl", "Unable to receive notice")
return Notice()
}
)
}

suspend operator fun invoke(nttId: String): Notice {
return getSingleNoticeById(nttId)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ package com.doyoonkim.knutice.fcm

import android.Manifest
import android.app.Notification
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.Icon
import android.os.Bundle
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.doyoonkim.knutice.data.KnuticeRemoteSource
import com.doyoonkim.knutice.R
import com.doyoonkim.knutice.presentation.MainActivity
import com.google.android.gms.tasks.OnCompleteListener
import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.FirebaseMessagingService
Expand All @@ -34,15 +38,19 @@ class PushNotificationHandler @Inject constructor() : FirebaseMessagingService()
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)

// When the app is in background or killed, Data Payload would be delivered once the user
// clicks the system tray.
Log.d(TAG, "Message data payload: ${message.notification}")

if (message.data.isNotEmpty()) {
Log.d(TAG, "Message Data Payload: ${message.data}")
}
Log.d(TAG, "Message Data Payload: ${message.data}") // message.data: Map<String!, String!>

message.notification?.let {
Log.d(TAG, "Body: ${it.body}")
message.toPushNotification()
// Apply "Do not disturb" option. (Temporarily save the message and deliver after the core time is end.
// Use Local Database (Room?)
message.notification?.let {
Log.d(TAG, "Body: ${it.body}")
message.toPushNotification()
}
}
}

Expand All @@ -51,13 +59,15 @@ class PushNotificationHandler @Inject constructor() : FirebaseMessagingService()
// Utilize channel already created by FCM as default
val notificationBuilder = NotificationCompat.Builder(
applicationContext, getString(R.string.inapp_notification_channel_id)
)
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(Icon.createWithResource(applicationContext, R.mipmap.ic_launcher))
.setContentTitle(getString(R.string.new_notice))
.setContentText(this@toPushNotification.notification?.body ?: "No message body")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
).apply {
setSmallIcon(R.mipmap.ic_launcher)
setLargeIcon(Icon.createWithResource(applicationContext, R.mipmap.ic_launcher))
setContentTitle(getString(R.string.new_notice))
setContentText(this@toPushNotification.notification?.body ?: "No message body.")
setPriority(NotificationCompat.PRIORITY_DEFAULT)
setAutoCancel(true)
}


with(NotificationManagerCompat.from(applicationContext)) {
if (ActivityCompat.checkSelfPermission(
Expand Down
48 changes: 46 additions & 2 deletions app/src/main/java/com/doyoonkim/knutice/model/DataWrappers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,46 @@ data class TopThreeNotices(
)
}

data class TopicSubscriptionStatusDTO(
@SerializedName("result") var result: Result? = Result(),
@SerializedName("body") var body: Body? = Body()
) {
data class Body(
@SerializedName("generalNewsTopic") var generalNewsTopic: Boolean = false,
@SerializedName("scholarshipNewsTopic") var scholarshipNewsTopic: Boolean = false,
@SerializedName("eventNewsTopic") var eventNewsTopic: Boolean = false,
@SerializedName("academicNewsTopic") var academicNewsTopic: Boolean = false,
@SerializedName("employmentNewsTopic") var employmentNewsTopic: Boolean = false
)
}

data class SingleNotice(
@SerializedName("result") var result: Result? = Result(),
@SerializedName("body") var notice: NoticeDTO?
) {
data class NoticeDTO(
@SerializedName("nttId") var nttId: Int,
@SerializedName("title") var title: String,
@SerializedName("contentUrl") var contentUrl: String,
@SerializedName("contentImage") var contentImage: String?,
@SerializedName("departmentName") var departmentName: String,
@SerializedName("registeredAt") var registeredAt: String,
@SerializedName("noticeName") var noticeName: String
) {
fun toNotice(): Notice {
return Notice(
nttId = nttId,
title = title,
url = contentUrl,
imageUrl = contentImage ?: "",
departName = departmentName,
timestamp = registeredAt,
// noticeCategory = noticeName
)
}
}
}

data class NoticesPerPage(
@SerializedName("result") var result: Result? = Result(),
@SerializedName("body") var body: ArrayList<RawNoticeData> = arrayListOf()
Expand Down Expand Up @@ -92,7 +132,8 @@ data class Notice(
title,
"[$departName] $timestamp",
url,
imageUrl
imageUrl,
nttId.toString()
)
}

Expand All @@ -111,6 +152,7 @@ data class Notice(

// DetailedNoticeContent
data class DetailedContentState(
val requestedNotice: Notice = Notice(),
val url: String = "",
val title: String = "",
val info: String = "",
Expand Down Expand Up @@ -140,7 +182,9 @@ data class SearchNoticeState(
data class NotificationPreferenceStatus(
val isMainNotificationPermissionGranted: Boolean = false,
//TODO: Consider change data type to MAP
val isEachChannelAllowed: List<Boolean> = listOf(true, true, true, true)
val isEachChannelAllowed: List<Boolean> = listOf(false, false, false, false),
val isSyncCompleted: Boolean = false,
val isError: Boolean = false
)

// BookmarkComposable
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/com/doyoonkim/knutice/model/Types.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ data class NavDestination(
val arrived: Destination = Destination.Unspecified
)


// TODO: Change structure of FullContent (FullContent(notice: Notice, url: String))
@Serializable
data class FullContent(
val title: String? = null,
val info: String? = null,
val url: String,
val imgUrl: String
val imgUrl: String,
val nttId: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import com.doyoonkim.knutice.viewModel.MainServiceViewModel
fun MainNavigator(
modifier: Modifier = Modifier,
viewModel: MainServiceViewModel = hiltViewModel(),
navController: NavHostController,
navController: NavHostController
) {
// Navigation
NavHost(
Expand Down Expand Up @@ -106,11 +106,21 @@ fun MainNavigator(
composable<FullContent> { backStackEntry ->
val requestedNotice = backStackEntry.toRoute<FullContent>()
val scaffoldTitle = requestedNotice.title
viewModel.updateState(
updatedCurrentLocation = Destination.DETAILED,
updatedCurrentScaffoldTitle = scaffoldTitle ?: "Full Content",
updatedBottomNavBarVisibility = true
)

viewModel.run {
if (uiState.value.currentLocation != Destination.DETAILED) {
updateState(
updatedCurrentLocation = Destination.DETAILED,
updatedCurrentScaffoldTitle = scaffoldTitle ?: "Full Content",
updatedBottomNavBarVisibility = true
)

// Need to find the reason for multiple request caused by multiple recomposition
if (requestedNotice.nttId != uiState.value.tempReserveNoticeForBookmark.nttId.toString()) {
getReservedNotice(requestedNotice.nttId)
}
}
}
DetailedNoticeContent()
Spacer(Modifier.height(20.dp))
}
Expand All @@ -120,7 +130,15 @@ fun MainNavigator(
updatedCurrentLocation = Destination.EDIT_BOOKMARK,
updatedBottomNavBarVisibility = false
)
EditBookmark(Modifier.padding(10.dp)) { bookmark ->
EditBookmark(
Modifier.padding(10.dp),
onDetailedNoticeRequested = {
viewModel.updateState(
updatedFabVisibility = false,
)
navController.navigate(it.toFullContent())
}
) { bookmark ->
Log.d("MainNavigator", "Bookmark instance received: ${bookmark ?: "null"}")
// onSavedClicked
if (bookmark != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.doyoonkim.knutice.R
import com.doyoonkim.knutice.model.Notice
import com.doyoonkim.knutice.presentation.component.NotificationPreviewCardMarked
import com.doyoonkim.knutice.ui.theme.containerBackgroundSolid
import com.doyoonkim.knutice.ui.theme.displayBackground
import com.doyoonkim.knutice.ui.theme.subTitle
import com.doyoonkim.knutice.viewModel.BookmarkViewModel
Expand All @@ -50,7 +51,7 @@ fun BookmarkComposable(
// }

Box(
modifier = modifier.background(MaterialTheme.colorScheme.displayBackground)
modifier = modifier.background(MaterialTheme.colorScheme.containerBackgroundSolid)
.systemBarsPadding()
) {
if (uiState.bookmarks.isEmpty()) {
Expand Down
Loading
Loading