mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-10 09:22:21 +00:00
add notif but not yet in background service
This commit is contained in:
@ -2,10 +2,10 @@ import java.util.Properties
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.jetbrains.kotlin.android)
|
||||
id("kotlin-kapt")
|
||||
alias(libs.plugins.ksp) // Use KSP instead of kapt
|
||||
id("androidx.navigation.safeargs")
|
||||
id("kotlin-parcelize")
|
||||
// id("com.google.dagger.hilt.android")
|
||||
alias(libs.plugins.dagger.hilt) // Use alias from catalog
|
||||
}
|
||||
|
||||
val localProperties = Properties().apply {
|
||||
@ -96,11 +96,27 @@ dependencies {
|
||||
|
||||
|
||||
// implementation(libs.hilt.android)
|
||||
// kapt("com.google.dagger:hilt-compiler:2.48")
|
||||
//
|
||||
// // For ViewModel injection (if needed)
|
||||
// implementation(libs.androidx.hilt.lifecycle.viewmodel)
|
||||
// kapt("androidx.hilt:hilt-compiler:1.0.0")
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.compiler)
|
||||
|
||||
// Androidx Hilt
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -6,12 +6,17 @@
|
||||
<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_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.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
|
||||
android:name=".app.App"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
@ -23,6 +28,18 @@
|
||||
android:theme="@style/Theme.Ecommerce_serang"
|
||||
android:usesCleartextTraffic="true"
|
||||
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
|
||||
android:name=".ui.order.detail.AddEvidencePaymentActivity"
|
||||
android:exported="false" />
|
||||
|
@ -1,7 +1,18 @@
|
||||
package com.alya.ecommerce_serang.app
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
//@HiltAndroidApp
|
||||
@HiltAndroidApp
|
||||
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)
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
package com.alya.ecommerce_serang.ui
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
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.ApiService
|
||||
import com.alya.ecommerce_serang.databinding.ActivityMainBinding
|
||||
import com.alya.ecommerce_serang.ui.notif.WebSocketManager
|
||||
import com.alya.ecommerce_serang.utils.SessionManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
//@AndroidEntryPoint
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var apiService: ApiService
|
||||
private lateinit var sessionManager: SessionManager
|
||||
// private val viewModel: NotifViewModel by viewModels()
|
||||
private val navController by lazy {
|
||||
(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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@ -47,9 +65,26 @@ class MainActivity : AppCompatActivity() {
|
||||
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()
|
||||
observeDestinationChanges()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (isFinishing) {
|
||||
webSocketManager.stopWebSocketConnection()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupBottomNavigation() {
|
||||
@ -78,7 +113,40 @@ class MainActivity : AppCompatActivity() {
|
||||
navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||
binding.bottomNavigation.isVisible = when (destination.id) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.repository.ProductRepository
|
||||
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.utils.BaseViewModelFactory
|
||||
import com.alya.ecommerce_serang.utils.HorizontalMarginItemDecoration
|
||||
@ -131,7 +132,8 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
|
||||
binding.searchContainer.btnNotification.setOnClickListener {
|
||||
// Navigate to notifications
|
||||
val intent = Intent(requireContext(), NotificationActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.utils
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
|
||||
class SessionManager(context: Context) {
|
||||
private var sharedPreferences: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
@ -10,12 +11,14 @@ class SessionManager(context: Context) {
|
||||
companion object {
|
||||
private const val PREFS_NAME = "app_prefs"
|
||||
private const val USER_TOKEN = "user_token"
|
||||
private const val USER_ID = "user_id" // New constant for storing user ID
|
||||
|
||||
}
|
||||
|
||||
fun saveToken(token: String) {
|
||||
val editor = sharedPreferences.edit()
|
||||
editor.putString(USER_TOKEN, token)
|
||||
editor.apply()
|
||||
sharedPreferences.edit() {
|
||||
putString(USER_TOKEN, token)
|
||||
}
|
||||
}
|
||||
|
||||
fun getToken(): String? {
|
||||
@ -24,9 +27,35 @@ class SessionManager(context: Context) {
|
||||
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() {
|
||||
val editor = sharedPreferences.edit()
|
||||
editor.remove(USER_TOKEN)
|
||||
editor.apply()
|
||||
sharedPreferences.edit() {
|
||||
remove(USER_TOKEN)
|
||||
}
|
||||
}
|
||||
|
||||
//clear data when log out
|
||||
fun clearAll() {
|
||||
sharedPreferences.edit() {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
}
|
5
app/src/main/res/drawable/baseline_alarm_24.xml
Normal file
5
app/src/main/res/drawable/baseline_alarm_24.xml
Normal 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>
|
29
app/src/main/res/layout/activity_notification.xml
Normal file
29
app/src/main/res/layout/activity_notification.xml
Normal 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>
|
@ -2,7 +2,6 @@
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath ("androidx.navigation:navigation-safe-args-gradle-plugin:2.5.1")
|
||||
// classpath("com.google.dagger:hilt-android-gradle-plugin:2.55")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
[versions]
|
||||
agp = "8.5.2"
|
||||
glide = "4.16.0"
|
||||
hiltAndroid = "2.51"
|
||||
hiltLifecycleViewmodel = "1.0.0-alpha03"
|
||||
agp = "8.9.2" # 8.7.2 is not an existing version, using latest stable 8.2.0
|
||||
hiltAndroid = "2.48" # Updated from 2.44 for better compatibility
|
||||
hiltCompiler = "2.48" # Added for consistency
|
||||
ksp = "1.9.0-1.0.13"
|
||||
kotlin = "1.9.0"
|
||||
|
||||
coreKtx = "1.10.1"
|
||||
@ -20,11 +20,10 @@ lifecycleViewmodelKtx = "2.8.7"
|
||||
fragmentKtx = "1.5.6"
|
||||
navigationFragmentKtx = "2.8.5"
|
||||
navigationUiKtx = "2.8.5"
|
||||
recyclerview = "1.4.0"
|
||||
recyclerview = "1.3.2"
|
||||
|
||||
[libraries]
|
||||
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" }
|
||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
|
||||
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-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]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
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" }
|
||||
|
||||
|
Reference in New Issue
Block a user