add notif but not yet in background service

This commit is contained in:
shaulascr
2025-04-23 17:12:45 +07:00
parent dfece2bfdf
commit 2bc4bda536
15 changed files with 698 additions and 28 deletions

View File

@ -2,10 +2,10 @@ import java.util.Properties
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.jetbrains.kotlin.android)
id("kotlin-kapt") alias(libs.plugins.ksp) // Use KSP instead of kapt
id ("androidx.navigation.safeargs") id("androidx.navigation.safeargs")
id("kotlin-parcelize") id("kotlin-parcelize")
// id("com.google.dagger.hilt.android") alias(libs.plugins.dagger.hilt) // Use alias from catalog
} }
val localProperties = Properties().apply { val localProperties = Properties().apply {
@ -96,11 +96,27 @@ dependencies {
// implementation(libs.hilt.android) // implementation(libs.hilt.android)
// kapt("com.google.dagger:hilt-compiler:2.48") implementation(libs.hilt.android)
// ksp(libs.hilt.compiler)
// // For ViewModel injection (if needed)
// implementation(libs.androidx.hilt.lifecycle.viewmodel) // Androidx Hilt
// kapt("androidx.hilt:hilt-compiler:1.0.0") implementation(libs.androidx.hilt.navigation.fragment)
implementation(libs.androidx.hilt.work)
ksp(libs.androidx.hilt.compiler)
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("androidx.work:work-runtime:2.8.1")
implementation("io.ktor:ktor-client-android:3.0.1")
implementation("io.ktor:ktor-client-core:3.0.1")
implementation("io.ktor:ktor-client-websockets:3.0.1")
implementation("io.ktor:ktor-client-logging:3.0.1")
implementation("io.ktor:ktor-client-okhttp:3.0.1")
implementation("io.ktor:ktor-client-content-negotiation:3.0.1")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.1")
implementation("io.socket:socket.io-client:2.1.0") // or latest version
} }

View File

@ -6,12 +6,17 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/> <uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application <application
android:name=".app.App"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@ -23,6 +28,18 @@
android:theme="@style/Theme.Ecommerce_serang" android:theme="@style/Theme.Ecommerce_serang"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<!-- <provider-->
<!-- android:name="androidx.startup.InitializationProvider"-->
<!-- android:authorities="${applicationId}.androidx-startup"-->
<!-- tools:node="remove" />-->
<service
android:name=".ui.notif.SimpleWebSocketService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
<activity
android:name=".ui.notif.NotificationActivity"
android:exported="false" />
<activity <activity
android:name=".ui.order.detail.AddEvidencePaymentActivity" android:name=".ui.order.detail.AddEvidencePaymentActivity"
android:exported="false" /> android:exported="false" />

View File

@ -1,7 +1,18 @@
package com.alya.ecommerce_serang.app package com.alya.ecommerce_serang.app
import android.app.Application import android.app.Application
import dagger.hilt.android.HiltAndroidApp
//@HiltAndroidApp @HiltAndroidApp
class App : Application(){ class App : Application(){
// override fun onCreate() {
// super.onCreate()
//
// val sessionManager = SessionManager(this)
// if (sessionManager.getUserId() != null) {
// val serviceIntent = Intent(this, SimpleWebSocketService::class.java)
// startService(serviceIntent)
// }
// }
} }

View File

@ -0,0 +1,89 @@
package com.alya.ecommerce_serang.di
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.graphics.Color
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.utils.SessionManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object NotificationModule {
@Provides
@Singleton
fun provideContext(@ApplicationContext context: Context): Context {
return context
}
@Provides
@Singleton
fun provideSessionManager(@ApplicationContext context: Context): SessionManager {
return SessionManager(context)
}
@Provides
@Singleton
fun provideApiService(sessionManager: SessionManager): ApiService {
return ApiConfig.getApiService(sessionManager)
}
@Provides
@Singleton
fun provideUserRepository(apiService: ApiService): UserRepository {
return UserRepository(apiService)
}
@Singleton
@Provides
fun provideNotificationBuilder(
@ApplicationContext context: Context
): NotificationCompat.Builder {
// Create a unique channel ID for your app
val channelId = "websocket_notifications"
// Ensure the notification channel exists
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"WebSocket Notifications",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notifications received via WebSocket"
enableLights(true)
lightColor = Color.BLUE
enableVibration(true)
vibrationPattern = longArrayOf(0, 1000, 500, 1000)
}
val notificationManager = context.getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
return NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.baseline_alarm_24)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
}
@Singleton
@Provides
fun provideNotificationManager(
@ApplicationContext context: Context
): NotificationManagerCompat {
return NotificationManagerCompat.from(context)
}
}

View File

@ -1,8 +1,13 @@
package com.alya.ecommerce_serang.ui package com.alya.ecommerce_serang.ui
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
@ -13,18 +18,31 @@ import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.databinding.ActivityMainBinding import com.alya.ecommerce_serang.databinding.ActivityMainBinding
import com.alya.ecommerce_serang.ui.notif.WebSocketManager
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
//@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var apiService: ApiService private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
// private val viewModel: NotifViewModel by viewModels()
private val navController by lazy { private val navController by lazy {
(supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController
} }
companion object{
private const val NOTIFICATION_PERMISSION_CODE = 100
}
@Inject
lateinit var webSocketManager: WebSocketManager
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@ -47,9 +65,26 @@ class MainActivity : AppCompatActivity() {
windowInsets windowInsets
} }
requestNotificationPermissionIfNeeded()
// Start WebSocket service through WebSocketManager after permission check
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
webSocketManager.startWebSocketConnection()
}
} else {
webSocketManager.startWebSocketConnection()
}
setupBottomNavigation() setupBottomNavigation()
observeDestinationChanges() observeDestinationChanges()
}
override fun onDestroy() {
super.onDestroy()
if (isFinishing) {
webSocketManager.stopWebSocketConnection()
}
} }
private fun setupBottomNavigation() { private fun setupBottomNavigation() {
@ -78,7 +113,40 @@ class MainActivity : AppCompatActivity() {
navController.addOnDestinationChangedListener { _, destination, _ -> navController.addOnDestinationChangedListener { _, destination, _ ->
binding.bottomNavigation.isVisible = when (destination.id) { binding.bottomNavigation.isVisible = when (destination.id) {
R.id.homeFragment, R.id.chatFragment, R.id.profileFragment -> true R.id.homeFragment, R.id.chatFragment, R.id.profileFragment -> true
else -> false // Bottom Navigation tidak terlihat di layar lain else -> false
}
}
}
private fun requestNotificationPermissionIfNeeded() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_CODE
)
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == NOTIFICATION_PERMISSION_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "Notification permission granted", Toast.LENGTH_SHORT).show()
webSocketManager.startWebSocketConnection()
} else {
Toast.makeText(this, "Notification permission denied", Toast.LENGTH_SHORT).show()
} }
} }
} }

View File

@ -21,6 +21,7 @@ import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ProductRepository import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.databinding.FragmentHomeBinding import com.alya.ecommerce_serang.databinding.FragmentHomeBinding
import com.alya.ecommerce_serang.ui.notif.NotificationActivity
import com.alya.ecommerce_serang.ui.product.DetailProductActivity import com.alya.ecommerce_serang.ui.product.DetailProductActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.HorizontalMarginItemDecoration import com.alya.ecommerce_serang.utils.HorizontalMarginItemDecoration
@ -131,7 +132,8 @@ class HomeFragment : Fragment() {
} }
binding.searchContainer.btnNotification.setOnClickListener { binding.searchContainer.btnNotification.setOnClickListener {
// Navigate to notifications val intent = Intent(requireContext(), NotificationActivity::class.java)
startActivity(intent)
} }
} }

View File

@ -0,0 +1,67 @@
package com.alya.ecommerce_serang.ui.notif
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.utils.SessionManager
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class NotifViewModel @Inject constructor(
private val notificationBuilder: NotificationCompat.Builder,
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context,
private val userRepository: UserRepository,
private val webSocketManager: WebSocketManager,
private val sessionManager: SessionManager
) : ViewModel() {
private val _userProfile = MutableStateFlow<Result<UserProfile?>>(Result.Loading)
val userProfile: StateFlow<Result<UserProfile?>> = _userProfile.asStateFlow()
init {
fetchUserProfile()
}
// Fetch user profile to get necessary data
fun fetchUserProfile() {
viewModelScope.launch {
_userProfile.value = Result.Loading
val result = userRepository.fetchUserProfile()
_userProfile.value = result
// If successful, save the user ID for WebSocket use
if (result is Result.Success && result.data != null) {
sessionManager.saveUserId(result.data.userId.toString())
}
}
}
// Start WebSocket connection
fun startWebSocketConnection() {
webSocketManager.startWebSocketConnection()
}
// Stop WebSocket connection
fun stopWebSocketConnection() {
webSocketManager.stopWebSocketConnection()
}
// Call when ViewModel is cleared (e.g., app closing)
override fun onCleared() {
super.onCleared()
// No need to stop here - the service will manage its own lifecycle
}
}

View File

@ -0,0 +1,118 @@
package com.alya.ecommerce_serang.ui.notif
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityNotificationBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint // Required for Hilt
class NotificationActivity : AppCompatActivity() {
private lateinit var binding: ActivityNotificationBinding
private val viewModel: NotifViewModel by viewModels()
// Permission request code
private val NOTIFICATION_PERMISSION_CODE = 100
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.userProfile.collect { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
// User profile loaded successfully
// Potentially do something with user profile
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
// Handle error - show message, etc.
Toast.makeText(this@NotificationActivity,
"Failed to load profile",
Toast.LENGTH_SHORT
).show()
}
Result.Loading -> {
// Show loading indicator if needed
}
}
}
}
}
// Start WebSocket connection
// viewModel.startWebSocketConnection()
binding = ActivityNotificationBinding.inflate(layoutInflater)
setContentView(binding.root)
// Check and request notification permission for Android 13+
requestNotificationPermissionIfNeeded()
// Set up button click listeners
// setupButtonListeners()
}
// private fun setupButtonListeners() {
// binding.simpleNotification.setOnClickListener {
// viewModel.showSimpleNotification()
// }
//
// binding.updateNotification.setOnClickListener {
// viewModel.updateSimpleNotification()
// }
//
// binding.cancelNotification.setOnClickListener {
// viewModel.cancelSimpleNotification()
// }
// }
private fun requestNotificationPermissionIfNeeded() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_CODE
)
}
}
}
// Handle permission request result
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == NOTIFICATION_PERMISSION_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted
Toast.makeText(this, "Notification permission granted", Toast.LENGTH_SHORT).show()
} else {
// Permission denied
Toast.makeText(this, "Notification permission denied", Toast.LENGTH_SHORT).show()
// You might want to show a dialog explaining why notifications are important
}
}
}
}

View File

@ -0,0 +1,162 @@
package com.alya.ecommerce_serang.ui.notif
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import com.alya.ecommerce_serang.BuildConfig
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.utils.SessionManager
import dagger.hilt.android.AndroidEntryPoint
import io.socket.client.IO
import io.socket.client.Socket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.json.JSONObject
import javax.inject.Inject
@AndroidEntryPoint
class SimpleWebSocketService : Service() {
companion object {
private const val TAG = "SocketIOService"
private const val NOTIFICATION_CHANNEL_ID = "websocket_service_channel"
private const val FOREGROUND_SERVICE_ID = 1001
}
@Inject
lateinit var notificationBuilder: NotificationCompat.Builder
@Inject
lateinit var notificationManager: NotificationManagerCompat
@Inject
lateinit var sessionManager: SessionManager
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var socket: Socket? = null
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Service created")
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
// .setSmallIcon(R.drawable.ic_notification) // Replace with your app's icon
.setPriority(NotificationCompat.PRIORITY_MIN) // Set the lowest priority
.setSound(null) // No sound
.setVibrate(longArrayOf(0L)) // No vibration
.setContentText("") // Empty text or minimal text
.setOngoing(true) // Keeps it ongoing
.build()
startForeground(1, notification)
startForeground(FOREGROUND_SERVICE_ID, notification)
serviceScope.launch { initSocket() }
return START_STICKY
}
private suspend fun initSocket() {
val userId = sessionManager.getUserId() ?: run {
Log.e(TAG, "User ID not available")
stopSelf()
return
}
val options = IO.Options().apply {
forceNew = true
reconnection = true
reconnectionDelay = 1000 // Retry every 1 second if disconnected
reconnectionAttempts = Int.MAX_VALUE
}
socket = IO.socket(BuildConfig.BASE_URL, options)
socket?.apply {
on(Socket.EVENT_CONNECT) {
Log.d(TAG, "Socket.IO connected")
emit("joinRoom", userId)
}
on("notification") { args ->
if (args.isNotEmpty()) {
val data = args[0] as? JSONObject
val title = data?.optString("title", "New Notification") ?: "Notification"
val message = data?.optString("message", "") ?: ""
showNotification(title, message)
}
}
on(Socket.EVENT_DISCONNECT) {
Log.d(TAG, "Socket.IO disconnected")
}
on(Socket.EVENT_CONNECT_ERROR) { args ->
Log.e(TAG, "Socket.IO connection error: ${args.firstOrNull()}")
}
connect()
}
}
private fun showNotification(title: String, message: String) {
val notification = notificationBuilder
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setSmallIcon(R.drawable.baseline_alarm_24)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
notificationManager.notify(System.currentTimeMillis().toInt(), notification)
} else {
Log.e(TAG, "Notification permission not granted")
}
} else {
notificationManager.notify(System.currentTimeMillis().toInt(), notification)
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
"WebSocket Service Channel",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Channel for WebSocket Service"
}
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(channel)
}
}
override fun onDestroy() {
Log.d(TAG, "Service destroyed")
socket?.disconnect()
socket?.off()
serviceScope.cancel()
super.onDestroy()
}
}

View File

@ -0,0 +1,51 @@
package com.alya.ecommerce_serang.ui.notif
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import com.alya.ecommerce_serang.utils.SessionManager
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class WebSocketManager @Inject constructor(
@ApplicationContext private val context: Context,
private val sessionManager: SessionManager
) {
companion object {
private const val TAG = "WebSocketManager"
}
fun startWebSocketConnection() {
try {
// Only start if we have a token
if (sessionManager.getToken().isNullOrEmpty()) {
Log.d(TAG, "No auth token available, not starting WebSocket service")
return
}
Log.d(TAG, "Starting WebSocket service")
val serviceIntent = Intent(context, SimpleWebSocketService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
} else {
context.startService(serviceIntent)
}
} catch (e: Exception) {
Log.e(TAG, "Error starting WebSocket service: ${e.message}")
}
}
fun stopWebSocketConnection() {
try {
Log.d(TAG, "Stopping WebSocket service")
context.stopService(Intent(context, SimpleWebSocketService::class.java))
} catch (e: Exception) {
Log.e(TAG, "Error stopping WebSocket service: ${e.message}")
}
}
}

View File

@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.utils
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log import android.util.Log
import androidx.core.content.edit
class SessionManager(context: Context) { class SessionManager(context: Context) {
private var sharedPreferences: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) private var sharedPreferences: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
@ -10,12 +11,14 @@ class SessionManager(context: Context) {
companion object { companion object {
private const val PREFS_NAME = "app_prefs" private const val PREFS_NAME = "app_prefs"
private const val USER_TOKEN = "user_token" private const val USER_TOKEN = "user_token"
private const val USER_ID = "user_id" // New constant for storing user ID
} }
fun saveToken(token: String) { fun saveToken(token: String) {
val editor = sharedPreferences.edit() sharedPreferences.edit() {
editor.putString(USER_TOKEN, token) putString(USER_TOKEN, token)
editor.apply() }
} }
fun getToken(): String? { fun getToken(): String? {
@ -24,9 +27,35 @@ class SessionManager(context: Context) {
return token return token
} }
fun saveUserId(userId: String) {
sharedPreferences.edit() {
putString(USER_ID, userId)
}
Log.d("SessionManager", "Saved user ID: $userId")
}
fun getUserId(): String? {
val userId = sharedPreferences.getString(USER_ID, null)
Log.d("SessionManager", "Retrieved user ID: $userId")
return userId
}
fun clearUserId() {
sharedPreferences.edit() {
remove(USER_ID)
}
}
fun clearToken() { fun clearToken() {
val editor = sharedPreferences.edit() sharedPreferences.edit() {
editor.remove(USER_TOKEN) remove(USER_TOKEN)
editor.apply() }
}
//clear data when log out
fun clearAll() {
sharedPreferences.edit() {
clear()
}
} }
} }

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M22,5.72l-4.6,-3.86 -1.29,1.53 4.6,3.86L22,5.72zM7.88,3.39L6.6,1.86 2,5.71l1.29,1.53 4.59,-3.85zM12.5,8L11,8v6l4.75,2.85 0.75,-1.23 -4,-2.37L12.5,8zM12,4c-4.97,0 -9,4.03 -9,9s4.02,9 9,9c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
</vector>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.notif.NotificationActivity">
<Button
android:id="@+id/simple_notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="simple notificaton"/>
<Button
android:id="@+id/update_notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="update notificaton"/>
<Button
android:id="@+id/cancel_notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="cancel notificaton"/>
</LinearLayout>

View File

@ -2,7 +2,6 @@
buildscript { buildscript {
dependencies { dependencies {
classpath ("androidx.navigation:navigation-safe-args-gradle-plugin:2.5.1") classpath ("androidx.navigation:navigation-safe-args-gradle-plugin:2.5.1")
// classpath("com.google.dagger:hilt-android-gradle-plugin:2.55")
} }
} }

View File

@ -1,8 +1,8 @@
[versions] [versions]
agp = "8.5.2" agp = "8.9.2" # 8.7.2 is not an existing version, using latest stable 8.2.0
glide = "4.16.0" hiltAndroid = "2.48" # Updated from 2.44 for better compatibility
hiltAndroid = "2.51" hiltCompiler = "2.48" # Added for consistency
hiltLifecycleViewmodel = "1.0.0-alpha03" ksp = "1.9.0-1.0.13"
kotlin = "1.9.0" kotlin = "1.9.0"
coreKtx = "1.10.1" coreKtx = "1.10.1"
@ -20,11 +20,10 @@ lifecycleViewmodelKtx = "2.8.7"
fragmentKtx = "1.5.6" fragmentKtx = "1.5.6"
navigationFragmentKtx = "2.8.5" navigationFragmentKtx = "2.8.5"
navigationUiKtx = "2.8.5" navigationUiKtx = "2.8.5"
recyclerview = "1.4.0" recyclerview = "1.3.2"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-hilt-lifecycle-viewmodel = { module = "androidx.hilt:hilt-lifecycle-viewmodel", version.ref = "hiltLifecycleViewmodel" }
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
@ -41,7 +40,15 @@ androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", ve
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" } androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" } androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltCompiler" }
androidx-hilt-common = { module = "androidx.hilt:hilt-common", version = "1.0.0" }
androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version = "1.0.0" }
androidx-hilt-navigation-fragment = { module = "androidx.hilt:hilt-navigation-fragment", version = "1.0.0" }
androidx-hilt-work = { module = "androidx.hilt:hilt-work", version = "1.0.0" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }