From 2bc4bda53607832a15becc5352f387888a3d73ed Mon Sep 17 00:00:00 2001 From: shaulascr Date: Wed, 23 Apr 2025 17:12:45 +0700 Subject: [PATCH] add notif but not yet in background service --- app/build.gradle.kts | 32 +++- app/src/main/AndroidManifest.xml | 23 ++- .../java/com/alya/ecommerce_serang/app/App.kt | 13 +- .../ecommerce_serang/di/NotificationModule.kt | 89 ++++++++++ .../alya/ecommerce_serang/ui/MainActivity.kt | 72 +++++++- .../ecommerce_serang/ui/home/HomeFragment.kt | 4 +- .../ui/notif/NotifViewModel.kt | 67 ++++++++ .../ui/notif/NotificationActivity.kt | 118 +++++++++++++ .../ui/notif/SimpleWebSocketService.kt | 162 ++++++++++++++++++ .../ui/notif/WebSocketManager.kt | 51 ++++++ .../ecommerce_serang/utils/SessionManager.kt | 41 ++++- .../main/res/drawable/baseline_alarm_24.xml | 5 + .../main/res/layout/activity_notification.xml | 29 ++++ build.gradle.kts | 1 - gradle/libs.versions.toml | 19 +- 15 files changed, 698 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/alya/ecommerce_serang/di/NotificationModule.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/notif/NotifViewModel.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/notif/NotificationActivity.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/notif/SimpleWebSocketService.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/notif/WebSocketManager.kt create mode 100644 app/src/main/res/drawable/baseline_alarm_24.xml create mode 100644 app/src/main/res/layout/activity_notification.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d01aae7..8afd6da 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,10 +2,10 @@ import java.util.Properties plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) - id("kotlin-kapt") - id ("androidx.navigation.safeargs") + 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 + } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4bae0a0..5af131b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,12 +6,17 @@ - - - + + + + + + + + + + + diff --git a/app/src/main/java/com/alya/ecommerce_serang/app/App.kt b/app/src/main/java/com/alya/ecommerce_serang/app/App.kt index 31a11eb..11361c4 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/app/App.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/app/App.kt @@ -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) +// } +// } + } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/di/NotificationModule.kt b/app/src/main/java/com/alya/ecommerce_serang/di/NotificationModule.kt new file mode 100644 index 0000000..b982df2 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/di/NotificationModule.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt index 0500df0..0c93bc6 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt @@ -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, + 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() } } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt index 381ad69..c19d02e 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/HomeFragment.kt @@ -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) } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/notif/NotifViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/NotifViewModel.kt new file mode 100644 index 0000000..d2040c4 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/NotifViewModel.kt @@ -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.Loading) + val userProfile: StateFlow> = _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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/notif/NotificationActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/NotificationActivity.kt new file mode 100644 index 0000000..1e22402 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/NotificationActivity.kt @@ -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, + 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 + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/notif/SimpleWebSocketService.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/SimpleWebSocketService.kt new file mode 100644 index 0000000..49913e7 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/SimpleWebSocketService.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/notif/WebSocketManager.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/WebSocketManager.kt new file mode 100644 index 0000000..f80f46b --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/WebSocketManager.kt @@ -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}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt index 6feaa5c..f04b37e 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/SessionManager.kt @@ -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() + } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_alarm_24.xml b/app/src/main/res/drawable/baseline_alarm_24.xml new file mode 100644 index 0000000..59acdcb --- /dev/null +++ b/app/src/main/res/drawable/baseline_alarm_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_notification.xml b/app/src/main/res/layout/activity_notification.xml new file mode 100644 index 0000000..e821fd0 --- /dev/null +++ b/app/src/main/res/layout/activity_notification.xml @@ -0,0 +1,29 @@ + + + +