From 4336142e1a70beddb28594d0081f0a83ac82a44d Mon Sep 17 00:00:00 2001 From: shaulascr Date: Mon, 21 Apr 2025 05:28:26 +0700 Subject: [PATCH 1/7] add search --- .../data/api/dto/RegisterRequest.kt | 8 +- .../data/api/dto/SearchRequest.kt | 8 + .../response/product/CreateSearchResponse.kt | 24 +++ .../response/product/SearchHistoryResponse.kt | 15 ++ .../data/api/retrofit/ApiService.kt | 21 ++- .../data/repository/ProductRepository.kt | 70 +++++++- .../ecommerce_serang/ui/home/HomeFragment.kt | 53 +++++- .../ui/home/SearchHistoryAdapter.kt | 56 ++++++ .../ui/home/SearchHomeFragment.kt | 162 ++++++++++++++++++ .../ui/home/SearchHomeViewModel.kt | 76 ++++++++ .../ui/home/SearchResultAdapter.kt | 78 +++++++++ .../utils/viewmodel/HomeViewModel.kt | 2 + app/src/main/res/layout/fragment_home.xml | 88 +++++++--- .../main/res/layout/fragment_search_home.xml | 76 ++++++++ app/src/main/res/layout/item_product_grid.xml | 67 ++++++++ .../main/res/layout/item_recent_search.xml | 32 ++++ app/src/main/res/navigation/nav_graph.xml | 16 +- 17 files changed, 809 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/dto/SearchRequest.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/CreateSearchResponse.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/SearchHistoryResponse.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHistoryAdapter.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeViewModel.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt create mode 100644 app/src/main/res/layout/fragment_search_home.xml create mode 100644 app/src/main/res/layout/item_product_grid.xml create mode 100644 app/src/main/res/layout/item_recent_search.xml diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt index 650eab3..19581dc 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt @@ -8,7 +8,11 @@ data class RegisterRequest ( val password: String?, val username: String?, val phone: String?, - @SerializedName("birth_date") val birthDate: String?, - @SerializedName("userimg") val image: String?, + @SerializedName("birth_date") + val birthDate: String?, + + @SerializedName("userimg") + val image: String?, + val otp: String? = null ) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/SearchRequest.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/SearchRequest.kt new file mode 100644 index 0000000..87c39cf --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/SearchRequest.kt @@ -0,0 +1,8 @@ +package com.alya.ecommerce_serang.data.api.dto + +import com.google.gson.annotations.SerializedName + +data class SearchRequest( + @SerializedName("search_query") + val searchQuery: String +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/CreateSearchResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/CreateSearchResponse.kt new file mode 100644 index 0000000..b6490bd --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/CreateSearchResponse.kt @@ -0,0 +1,24 @@ +package com.alya.ecommerce_serang.data.api.response.product + +import com.google.gson.annotations.SerializedName + +data class CreateSearchResponse( + + @field:SerializedName("search") + val search: Search +) + +data class Search( + + @field:SerializedName("user_id") + val userId: Int, + + @field:SerializedName("created_at") + val createdAt: String, + + @field:SerializedName("id") + val id: Int, + + @field:SerializedName("search_query") + val searchQuery: String +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/SearchHistoryResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/SearchHistoryResponse.kt new file mode 100644 index 0000000..43dabc0 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/product/SearchHistoryResponse.kt @@ -0,0 +1,15 @@ +package com.alya.ecommerce_serang.data.api.response.product + +import com.google.gson.annotations.SerializedName + +data class SearchHistoryResponse( + + @field:SerializedName("data") + val data: List +) + +data class DataItem( + + @field:SerializedName("search_query") + val searchQuery: String +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt index 1077a9c..d222885 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt @@ -10,11 +10,9 @@ import com.alya.ecommerce_serang.data.api.dto.OrderRequest import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy import com.alya.ecommerce_serang.data.api.dto.OtpRequest import com.alya.ecommerce_serang.data.api.dto.RegisterRequest +import com.alya.ecommerce_serang.data.api.dto.SearchRequest import com.alya.ecommerce_serang.data.api.dto.UpdateCart -import com.alya.ecommerce_serang.data.api.response.product.CreateProductResponse import com.alya.ecommerce_serang.data.api.response.ViewStoreProductsResponse -import okhttp3.MultipartBody -import okhttp3.RequestBody import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse @@ -32,21 +30,22 @@ import com.alya.ecommerce_serang.data.api.response.order.OrderDetailResponse import com.alya.ecommerce_serang.data.api.response.order.OrderListResponse import com.alya.ecommerce_serang.data.api.response.product.AllProductResponse import com.alya.ecommerce_serang.data.api.response.product.CategoryResponse +import com.alya.ecommerce_serang.data.api.response.product.CreateProductResponse +import com.alya.ecommerce_serang.data.api.response.product.CreateSearchResponse import com.alya.ecommerce_serang.data.api.response.product.DetailStoreProductResponse import com.alya.ecommerce_serang.data.api.response.product.ProductResponse import com.alya.ecommerce_serang.data.api.response.product.ReviewProductResponse +import com.alya.ecommerce_serang.data.api.response.product.SearchHistoryResponse import com.alya.ecommerce_serang.data.api.response.product.StoreResponse import com.alya.ecommerce_serang.data.api.response.profile.AddressResponse import com.alya.ecommerce_serang.data.api.response.profile.CreateAddressResponse import com.alya.ecommerce_serang.data.api.response.profile.ProfileResponse +import okhttp3.MultipartBody +import okhttp3.RequestBody import retrofit2.Call import retrofit2.Response import retrofit2.http.Body -import retrofit2.http.Field -import retrofit2.http.FormUrlEncoded import retrofit2.http.GET -import retrofit2.http.Header -import retrofit2.http.HeaderMap import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.PUT @@ -202,4 +201,12 @@ interface ApiService { @Part("description") description: RequestBody, @Part complaintimg: MultipartBody.Part ): Response + + @POST("search") + suspend fun saveSearchQuery( + @Body searchRequest: SearchRequest + ): Response + + @GET("search") + suspend fun getSearchHistory(): Response } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt index 1a8781c..c51008e 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ProductRepository.kt @@ -4,19 +4,19 @@ import android.util.Log import com.alya.ecommerce_serang.data.api.dto.CartItem import com.alya.ecommerce_serang.data.api.dto.CategoryItem import com.alya.ecommerce_serang.data.api.dto.ProductsItem -import com.alya.ecommerce_serang.data.api.response.product.CreateProductResponse +import com.alya.ecommerce_serang.data.api.dto.SearchRequest import com.alya.ecommerce_serang.data.api.response.cart.AddCartResponse +import com.alya.ecommerce_serang.data.api.response.product.CreateProductResponse import com.alya.ecommerce_serang.data.api.response.product.ProductResponse import com.alya.ecommerce_serang.data.api.response.product.ReviewsItem +import com.alya.ecommerce_serang.data.api.response.product.Search import com.alya.ecommerce_serang.data.api.response.product.StoreProduct import com.alya.ecommerce_serang.data.api.retrofit.ApiService -import com.alya.ecommerce_serang.utils.SessionManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody -import java.io.File class ProductRepository(private val apiService: ApiService) { suspend fun getAllProducts(): Result> = @@ -192,6 +192,70 @@ class ProductRepository(private val apiService: ApiService) { } } + suspend fun searchProducts(query: String): Result> = + withContext(Dispatchers.IO) { + try { + // First save the search query + saveSearchQuery(query) + + // Then fetch all products + val response = apiService.getAllProduct() + + if (response.isSuccessful) { + val allProducts = response.body()?.products ?: emptyList() + + // Filter products based on the search query + val filteredProducts = allProducts.filter { product -> + product.name.contains(query, ignoreCase = true) || + (product.description?.contains(query, ignoreCase = true) ?: false) + } + + Log.d(TAG, "Found ${filteredProducts.size} products matching '$query'") + Result.Success(filteredProducts) + } else { + Result.Error(Exception("Failed to fetch products for search. Code: ${response.code()}")) + } + } catch (e: Exception) { + Log.e(TAG, "Error searching products", e) + Result.Error(e) + } + } + + suspend fun saveSearchQuery(query: String): Result = + withContext(Dispatchers.IO) { + try { + val response = apiService.saveSearchQuery(SearchRequest(query)) + + if (response.isSuccessful) { + Result.Success(response.body()?.search) + } else { + Log.e(TAG, "Failed to save search query. Code: ${response.code()}") + Result.Error(Exception("Failed to save search query")) + } + } catch (e: Exception) { + Log.e(TAG, "Error saving search query", e) + Result.Error(e) + } + } + + suspend fun getSearchHistory(): Result> = + withContext(Dispatchers.IO) { + try { + val response = apiService.getSearchHistory() + + if (response.isSuccessful) { + val searches = response.body()?.data?.map { it.searchQuery } ?: emptyList() + Result.Success(searches) + } else { + Log.e(TAG, "Failed to fetch search history. Code: ${response.code()}") + Result.Error(Exception("Failed to fetch search history")) + } + } catch (e: Exception) { + Log.e(TAG, "Error fetching search history", e) + Result.Error(e) + } + } + companion object { private const val TAG = "ProductRepository" 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 c0dd046..b9b6fc4 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 @@ -6,12 +6,14 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.inputmethod.EditorInfo import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.findNavController import androidx.recyclerview.widget.LinearLayoutManager import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.data.api.dto.CategoryItem @@ -63,6 +65,8 @@ class HomeFragment : Fragment() { initUi() setupRecyclerView() observeData() + setupSearchView() + } private fun setupRecyclerView() { @@ -95,6 +99,40 @@ class HomeFragment : Fragment() { } } + private fun setupSearchView() { + binding.searchContainer.search.apply { + // When user clicks the search box, navigate to search fragment + setOnClickListener { + findNavController().navigate( + HomeFragmentDirections.actionHomeFragmentToSearchHomeFragment(null) + ) + } + +// Handle search action if user presses search on keyboard + setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + val query = text.toString().trim() + if (query.isNotEmpty()) { + findNavController().navigate( + HomeFragmentDirections.actionHomeFragmentToSearchHomeFragment(query) + ) + } + return@setOnEditorActionListener true + } + false + } + } + + // Setup cart and notification buttons + binding.searchContainer.btnCart.setOnClickListener { + // Navigate to cart + } + + binding.searchContainer.btnNotification.setOnClickListener { + // Navigate to notifications + } + } + private fun observeData() { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -109,7 +147,7 @@ class HomeFragment : Fragment() { binding.loading.root.isVisible = false binding.error.root.isVisible = false binding.home.isVisible = true - productAdapter?.updateLimitedProducts(state.products) // Ensure productAdapter is initialized + productAdapter?.updateLimitedProducts(state.products) } is HomeUiState.Error -> { binding.loading.root.isVisible = false @@ -125,18 +163,16 @@ class HomeFragment : Fragment() { } } - viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.categories.collect { categories -> Log.d("Categories", "Updated Categories: $categories") - categories.forEach { Log.d("Category Image", "Category: ${it.name}, Image: ${it.image}") } categoryAdapter?.updateLimitedCategory(categories) } } } } - private fun initUi() { // For LightStatusBar setLightStatusBar() @@ -161,7 +197,6 @@ class HomeFragment : Fragment() { ) } - private fun handleProductClick(product: ProductsItem) { val intent = Intent(requireContext(), DetailProductActivity::class.java) intent.putExtra("PRODUCT_ID", product.id) // Pass product ID @@ -169,7 +204,7 @@ class HomeFragment : Fragment() { } private fun handleCategoryProduct(category: CategoryItem) { - + // Your implementation } override fun onDestroyView() { @@ -179,7 +214,7 @@ class HomeFragment : Fragment() { _binding = null } - private fun showLoading(isLoading: Boolean) { - binding.progressBar.isVisible = isLoading - } +// private fun showLoading(isLoading: Boolean) { +// binding.progressBar.isVisible = isLoading +// } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHistoryAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHistoryAdapter.kt new file mode 100644 index 0000000..a00b3d7 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHistoryAdapter.kt @@ -0,0 +1,56 @@ +package com.alya.ecommerce_serang.ui.home + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.alya.ecommerce_serang.databinding.ItemRecentSearchBinding + +class SearchHistoryAdapter( + private val onItemClick: (String) -> Unit +) : ListAdapter(DIFF_CALLBACK) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ItemRecentSearchBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val query = getItem(position) + holder.bind(query) + } + + inner class ViewHolder(private val binding: ItemRecentSearchBinding) : + RecyclerView.ViewHolder(binding.root) { + + init { + binding.root.setOnClickListener { + val position = adapterPosition + if (position != RecyclerView.NO_POSITION) { + onItemClick(getItem(position)) + } + } + } + + fun bind(query: String) { + binding.recentSearchText.text = query + } + } + + companion object { + private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: String, newItem: String): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: String, newItem: String): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt new file mode 100644 index 0000000..94003c6 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeFragment.kt @@ -0,0 +1,162 @@ +package com.alya.ecommerce_serang.ui.home + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.GridLayoutManager +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.FragmentSearchHomeBinding +import com.alya.ecommerce_serang.ui.product.DetailProductActivity +import com.alya.ecommerce_serang.utils.BaseViewModelFactory +import com.alya.ecommerce_serang.utils.SessionManager + +class SearchHomeFragment : Fragment() { + private var _binding: FragmentSearchHomeBinding? = null + private val binding get() = _binding!! + private var searchResultsAdapter: SearchResultsAdapter? = null + private lateinit var sessionManager: SessionManager + private val args: SearchHomeFragmentArgs by navArgs() + + private val viewModel: SearchHomeViewModel by viewModels { + BaseViewModelFactory { + val apiService = ApiConfig.getApiService(sessionManager) + val productRepository = ProductRepository(apiService) + SearchHomeViewModel(productRepository) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sessionManager = SessionManager(requireContext()) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSearchHomeBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUI() + setupSearchResultsRecyclerView() + observeData() + + // Perform search with the query passed from HomeFragment + args.query?.let { query -> + // Wait until layout is done, then set query text + binding.searchView.post { + binding.searchView.setQuery(query, false) // sets "food" as text, doesn't submit + } + + viewModel.searchProducts(query) + } + } + + private fun setupUI() { + // Setup back button + binding.backButton.setOnClickListener { + findNavController().navigateUp() + } + + // Setup search view + binding.searchView.apply { + setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + query?.let { + if (it.isNotEmpty()) { + viewModel.searchProducts(it) + hideKeyboard() + } + } + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + newText?.let { + if (it.isEmpty()) { + // Clear the search results if user clears the input + searchResultsAdapter?.submitList(emptyList()) + binding.noResultsText.isVisible = false + return true + } + + // Optional: do real-time search + if (it.length >= 2) { + viewModel.searchProducts(it) + } + } + return true + } + }) + + // Request focus and show keyboard + if (args.query.isNullOrEmpty()) { + requestFocus() + postDelayed({ + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(findFocus(), InputMethodManager.SHOW_IMPLICIT) + }, 200) + } + } + } + + private fun setupSearchResultsRecyclerView() { + searchResultsAdapter = SearchResultsAdapter { product -> + navigateToProductDetail(product) + } + + binding.searchResultsRecyclerView.apply { + adapter = searchResultsAdapter + layoutManager = GridLayoutManager(requireContext(), 2) + } + } + + private fun observeData() { + viewModel.searchResults.observe(viewLifecycleOwner) { products -> + + searchResultsAdapter?.submitList(products) + binding.noResultsText.isVisible = products.isEmpty() && !viewModel.isSearching.value!! + binding.searchResultsRecyclerView.isVisible = products.isNotEmpty() + } + + viewModel.isSearching.observe(viewLifecycleOwner) { isSearching -> + binding.progressBar.isVisible = isSearching + } + } + + private fun navigateToProductDetail(product: ProductsItem) { + val intent = Intent(requireContext(), DetailProductActivity::class.java) + intent.putExtra("PRODUCT_ID", product.id) + startActivity(intent) + } + + private fun hideKeyboard() { + val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + binding.searchView.let { + imm.hideSoftInputFromWindow(it.windowToken, 0) + } + } + + + override fun onDestroyView() { + super.onDestroyView() + searchResultsAdapter = null + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeViewModel.kt new file mode 100644 index 0000000..fec5c4e --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchHomeViewModel.kt @@ -0,0 +1,76 @@ +package com.alya.ecommerce_serang.ui.home + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.alya.ecommerce_serang.data.api.dto.ProductsItem +import com.alya.ecommerce_serang.data.repository.ProductRepository +import com.alya.ecommerce_serang.data.repository.Result +import kotlinx.coroutines.launch + +class SearchHomeViewModel (private val productRepository: ProductRepository) : ViewModel() { + + private val _searchResults = MutableLiveData>(emptyList()) + val searchResults: LiveData> = _searchResults + + private val _searchHistory = MutableLiveData>(emptyList()) + val searchHistory: LiveData> = _searchHistory + + private val _isSearching = MutableLiveData(false) + val isSearching: LiveData = _isSearching + + private val _isSearchActive = MutableLiveData(false) + val isSearchActive: LiveData = _isSearchActive + + fun searchProducts(query: String) { + Log.d("HomeViewModel", "searchProducts called with query: '$query'") + + if (query.isBlank()) { + Log.d("HomeViewModel", "Query is blank, clearing results") + _searchResults.value = emptyList() + _isSearchActive.value = false + return + } + + _isSearching.value = true + _isSearchActive.value = true + + viewModelScope.launch { + Log.d("HomeViewModel", "Starting search coroutine") + + when (val result = productRepository.searchProducts(query)) { + is Result.Success -> { + Log.d("HomeViewModel", "Search successful, found ${result.data.size} products") + _searchResults.postValue(result.data) + + // Double check the state after assignment + Log.d("HomeViewModel", "Updated searchResults value has ${result.data.size} items") + } + is Result.Error -> { + Log.e("HomeViewModel", "Search failed", result.exception) + _searchResults.postValue(emptyList()) + } + else -> {} + } + _isSearching.postValue(false) + } + } + + fun clearSearch() { + _isSearchActive.value = false + _searchResults.value = emptyList() + _isSearching.value = false + } + + fun loadSearchHistory() { + viewModelScope.launch { + when (val result = productRepository.getSearchHistory()) { + is Result.Success -> _searchHistory.value = result.data + is Result.Error -> Log.e("HomeViewModel", "Failed to load search history", result.exception) + else -> {} + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt new file mode 100644 index 0000000..7a95fdc --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/home/SearchResultAdapter.kt @@ -0,0 +1,78 @@ +package com.alya.ecommerce_serang.ui.home + +import android.util.Log +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.dto.ProductsItem +import com.alya.ecommerce_serang.databinding.ItemProductGridBinding +import com.bumptech.glide.Glide + +class SearchResultsAdapter( + private val onItemClick: (ProductsItem) -> Unit +) : ListAdapter(DIFF_CALLBACK) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ItemProductGridBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val product = getItem(position) + holder.bind(product) + } + + inner class ViewHolder(private val binding: ItemProductGridBinding) : + RecyclerView.ViewHolder(binding.root) { + + init { + binding.root.setOnClickListener { + val position = adapterPosition + if (position != RecyclerView.NO_POSITION) { + onItemClick(getItem(position)) + } + } + } + + fun bind(product: ProductsItem) { + binding.productName.text = product.name + binding.productPrice.text = (product.price) + + // Load image with Glide + Glide.with(binding.root.context) + .load(product.image) + .placeholder(R.drawable.placeholder_image) +// .error(R.drawable.error_image) + .into(binding.productImage) + + // Set store name if available + product.storeId?.toString().let { + binding.storeName.text = it + } + } + } + + override fun submitList(list: List?) { + Log.d("SearchResultsAdapter", "Submitting list with ${list?.size ?: 0} items") + super.submitList(list) + } + + companion object { + private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ProductsItem, newItem: ProductsItem): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: ProductsItem, newItem: ProductsItem): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt index 0e321ba..0f0437b 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/HomeViewModel.kt @@ -52,6 +52,8 @@ class HomeViewModel ( loadProducts() loadCategories() } + + } sealed class HomeUiState { diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 4deae79..1ce0c9b 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -6,34 +6,28 @@ xmlns:app="http://schemas.android.com/apk/res-auto" tools:context=".ui.home.HomeFragment"> + + + + android:layout_height="0dp" + app:layout_constraintTop_toBottomOf="@id/searchContainer" + app:layout_constraintBottom_toBottomOf="parent"> + - - - + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_search_home.xml b/app/src/main/res/layout/fragment_search_home.xml new file mode 100644 index 0000000..69eb47b --- /dev/null +++ b/app/src/main/res/layout/fragment_search_home.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_product_grid.xml b/app/src/main/res/layout/item_product_grid.xml new file mode 100644 index 0000000..cebc279 --- /dev/null +++ b/app/src/main/res/layout/item_product_grid.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_recent_search.xml b/app/src/main/res/layout/item_recent_search.xml new file mode 100644 index 0000000..c098004 --- /dev/null +++ b/app/src/main/res/layout/item_recent_search.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 99d7a48..ffe5064 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -9,7 +9,11 @@ android:id="@+id/homeFragment" android:name="com.alya.ecommerce_serang.ui.home.HomeFragment" android:label="fragment_home" - tools:layout="@layout/fragment_home" /> + tools:layout="@layout/fragment_home"> + + + + + Date: Tue, 22 Apr 2025 04:10:08 +0700 Subject: [PATCH 2/7] fix regist and ui home --- .../data/api/dto/RegisterRequest.kt | 2 +- .../alya/ecommerce_serang/ui/MainActivity.kt | 22 ++++++++- .../ui/auth/RegisterActivity.kt | 45 ++++++++++++++++++- .../ecommerce_serang/ui/home/HomeFragment.kt | 2 + .../ui/product/DetailProductActivity.kt | 20 +++++++++ .../ui/profile/ProfileFragment.kt | 1 - .../main/res/color/bottom_nav_icon_color.xml | 5 +++ .../main/res/color/bottom_nav_text_color.xml | 5 +++ .../res/drawable/bottom_nav_background.xml | 10 +++++ .../drawable/outline_calendar_today_24.xml | 5 +++ .../res/layout/activity_detail_product.xml | 7 +-- app/src/main/res/layout/activity_main.xml | 32 ++++++------- app/src/main/res/layout/activity_register.xml | 16 ++++--- app/src/main/res/layout/fragment_home.xml | 22 ++++----- app/src/main/res/layout/fragment_profile.xml | 8 +--- app/src/main/res/layout/view_search.xml | 1 + app/src/main/res/values/colors.xml | 5 +++ app/src/main/res/values/styles.xml | 35 +++++++++++++++ app/src/main/res/values/themes.xml | 39 +++++++++++++--- 19 files changed, 231 insertions(+), 51 deletions(-) create mode 100644 app/src/main/res/color/bottom_nav_icon_color.xml create mode 100644 app/src/main/res/color/bottom_nav_text_color.xml create mode 100644 app/src/main/res/drawable/bottom_nav_background.xml create mode 100644 app/src/main/res/drawable/outline_calendar_today_24.xml diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt index 19581dc..d1a4482 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/RegisterRequest.kt @@ -12,7 +12,7 @@ data class RegisterRequest ( val birthDate: String?, @SerializedName("userimg") - val image: String?, + val image: String? = null, val otp: String? = null ) \ 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 3877327..0500df0 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,7 +1,11 @@ package com.alya.ecommerce_serang.ui import android.os.Bundle +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController @@ -25,7 +29,23 @@ class MainActivity : AppCompatActivity() { setContentView(binding.root) sessionManager = SessionManager(this) - apiService = ApiConfig.getApiService(sessionManager) // Inject SessionManager + apiService = ApiConfig.getApiService(sessionManager) + + WindowCompat.setDecorFitsSystemWindows(window, false) + + enableEdgeToEdge() + + // Apply insets to your root layout + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> + val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.setPadding( + systemBars.left, + systemBars.top, + systemBars.right, + 0 + ) + windowInsets + } setupBottomNavigation() observeDestinationChanges() diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt index ec126d1..dbe2793 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt @@ -1,5 +1,6 @@ package com.alya.ecommerce_serang.ui.auth +import android.app.DatePickerDialog import android.content.Intent import android.os.Bundle import android.util.Log @@ -7,6 +8,9 @@ import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat import com.alya.ecommerce_serang.data.api.dto.RegisterRequest import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.repository.Result @@ -16,6 +20,9 @@ import com.alya.ecommerce_serang.ui.MainActivity import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale class RegisterActivity : AppCompatActivity() { private lateinit var binding: ActivityRegisterBinding @@ -38,10 +45,24 @@ class RegisterActivity : AppCompatActivity() { finish() } + WindowCompat.setDecorFitsSystemWindows(window, false) + enableEdgeToEdge() + + // Apply insets to your root layout + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> + val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.setPadding( + systemBars.left, + systemBars.top, + systemBars.right, + systemBars.bottom + ) + windowInsets + } + binding = ActivityRegisterBinding.inflate(layoutInflater) setContentView(binding.root) - // Observe OTP state observeOtpState() @@ -53,7 +74,7 @@ class RegisterActivity : AppCompatActivity() { val phone = binding.etNumberPhone.text.toString() val username = binding.etUsername.text.toString() val name = binding.etFullname.text.toString() - val image = "not yet" + val image = null val userData = RegisterRequest(name, email, password, username, phone, birthDate, image) @@ -94,6 +115,9 @@ class RegisterActivity : AppCompatActivity() { startActivity(intent) } + binding.etBirthDate.setOnClickListener{ + showDatePicker() + } } private fun observeOtpState() { @@ -140,4 +164,21 @@ class RegisterActivity : AppCompatActivity() { } } } + + private fun showDatePicker() { + val calendar = Calendar.getInstance() + val year = calendar.get(Calendar.YEAR) + val month = calendar.get(Calendar.MONTH) + val day = calendar.get(Calendar.DAY_OF_MONTH) + + DatePickerDialog( + this, + { _, selectedYear, selectedMonth, selectedDay -> + calendar.set(selectedYear, selectedMonth, selectedDay) + val sdf = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()) + binding.etBirthDate.setText(sdf.format(calendar.time)) + }, + year, month, day + ).show() + } } \ No newline at end of file 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 b9b6fc4..381ad69 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 @@ -49,6 +49,7 @@ class HomeFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) sessionManager = SessionManager(requireContext()) + } override fun onCreateView( @@ -62,6 +63,7 @@ class HomeFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + initUi() setupRecyclerView() observeData() diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt index bb10bdb..4be3e57 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/product/DetailProductActivity.kt @@ -9,8 +9,12 @@ import android.widget.Button import android.widget.ImageButton import android.widget.TextView import android.widget.Toast +import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat import androidx.recyclerview.widget.LinearLayoutManager import com.alya.ecommerce_serang.BuildConfig.BASE_URL import com.alya.ecommerce_serang.R @@ -57,6 +61,22 @@ class DetailProductActivity : AppCompatActivity() { sessionManager = SessionManager(this) apiService = ApiConfig.getApiService(sessionManager) + WindowCompat.setDecorFitsSystemWindows(window, false) + + enableEdgeToEdge() + + // Apply insets to your root layout + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> + val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.setPadding( + systemBars.left, + systemBars.top, + systemBars.right, + systemBars.bottom + ) + windowInsets + } + setupUI() setupObservers() loadData() diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt index b527fd9..d1921f9 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/ProfileFragment.kt @@ -39,7 +39,6 @@ class ProfileFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) sessionManager = SessionManager(requireContext()) - } override fun onCreateView( diff --git a/app/src/main/res/color/bottom_nav_icon_color.xml b/app/src/main/res/color/bottom_nav_icon_color.xml new file mode 100644 index 0000000..1da8b16 --- /dev/null +++ b/app/src/main/res/color/bottom_nav_icon_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/bottom_nav_text_color.xml b/app/src/main/res/color/bottom_nav_text_color.xml new file mode 100644 index 0000000..481be1a --- /dev/null +++ b/app/src/main/res/color/bottom_nav_text_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bottom_nav_background.xml b/app/src/main/res/drawable/bottom_nav_background.xml new file mode 100644 index 0000000..68362a6 --- /dev/null +++ b/app/src/main/res/drawable/bottom_nav_background.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_calendar_today_24.xml b/app/src/main/res/drawable/outline_calendar_today_24.xml new file mode 100644 index 0000000..5ca556c --- /dev/null +++ b/app/src/main/res/drawable/outline_calendar_today_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_detail_product.xml b/app/src/main/res/layout/activity_detail_product.xml index f9182d4..65458ad 100644 --- a/app/src/main/res/layout/activity_detail_product.xml +++ b/app/src/main/res/layout/activity_detail_product.xml @@ -5,7 +5,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/white" + android:theme="@style/Theme.Ecommerce_serang" tools:context=".ui.product.DetailProductActivity"> @@ -147,7 +147,8 @@ android:id="@+id/recyclerViewReviews" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="8dp" + android:layout_marginTop="4dp" + android:layout_marginBottom="8dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:itemCount="1" tools:listitem="@layout/item_review" /> @@ -392,6 +393,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" + android:layout_marginBottom="8dp" android:orientation="horizontal" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:itemCount="3" @@ -419,7 +421,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" - android:backgroundTint="@color/white" app:contentInsetStart="0dp"> - + app:itemIconSize="32dp" + app:itemPaddingBottom="4dp" + app:itemTextAppearanceActive="@style/BottomNavigationTextStyle" + app:itemTextAppearanceInactive="@style/BottomNavigationTextStyle" + android:elevation="8dp" + app:itemIconTint="@color/bottom_nav_icon_color" + app:itemTextColor="@color/bottom_nav_text_color" /> - + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_register.xml b/app/src/main/res/layout/activity_register.xml index a23ce3e..6485770 100644 --- a/app/src/main/res/layout/activity_register.xml +++ b/app/src/main/res/layout/activity_register.xml @@ -5,7 +5,7 @@ android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/white" + android:theme="@style/Theme.Ecommerce_serang" tools:context=".ui.auth.RegisterActivity"> + + style="@style/SharpedBorderStyleOutline" + app:endIconMode="custom" + app:endIconDrawable="@drawable/outline_calendar_today_24"> - + android:hint="Pilih tanggal" + android:focusable="false" + android:clickable="true" + android:minHeight="50dp"/> @@ -58,8 +61,8 @@ android:layout_marginEnd="32dp" android:text="@string/show_all" android:textAllCaps="false" - android:textColor="@color/blue1" - android:textSize="16sp" + android:textColor="@color/blue_600" + android:textSize="14sp" app:layout_constraintBaseline_toBaselineOf="@id/categoriesText" app:layout_constraintEnd_toEndOf="parent" /> @@ -67,7 +70,7 @@ android:id="@+id/categories" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="19dp" + android:layout_marginTop="8dp" android:clipChildren="false" android:clipToPadding="false" android:orientation="horizontal" @@ -77,16 +80,15 @@ tools:layout_editor_absoluteX="0dp" tools:listitem="@layout/item_category_home" /> - - @@ -98,7 +100,7 @@ android:layout_marginEnd="32dp" android:text="@string/show_all" android:textAllCaps="false" - android:textColor="@color/blue1" + android:textColor="@color/blue_600" android:textSize="16sp" app:layout_constraintBaseline_toBaselineOf="@id/new_products_text" app:layout_constraintEnd_toEndOf="parent" /> diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml index afbd7ec..22363c9 100644 --- a/app/src/main/res/layout/fragment_profile.xml +++ b/app/src/main/res/layout/fragment_profile.xml @@ -5,6 +5,7 @@ android:layout_height="match_parent" android:background="@color/white" xmlns:app="http://schemas.android.com/apk/res-auto" + android:theme="@style/Theme.Ecommerce_serang" tools:context=".ui.profile.ProfileFragment"> @@ -320,12 +321,5 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/tvLogout" /> - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_search.xml b/app/src/main/res/layout/view_search.xml index 2a2a3c9..3f7d5b6 100644 --- a/app/src/main/res/layout/view_search.xml +++ b/app/src/main/res/layout/view_search.xml @@ -12,6 +12,7 @@ android:hint="@string/fragment_home_search" android:textColor="@color/soft_gray" android:textSize="16sp" + android:fontFamily="@font/dmsans_regular" android:layout_marginStart="8dp" android:drawablePadding="8dp" android:paddingHorizontal="25dp" diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 8aa4f32..25b40eb 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -41,4 +41,9 @@ #E8ECF2 #7D8FAB #489EC6 + + #489EC6 + #8E8E8E + #489EC6 + #8E8E8E \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 06b23b1..dcf0226 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -25,4 +25,39 @@ 8dp @color/blue_500 + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 0da93ba..24782af 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,16 +1,45 @@ From 2bc4bda53607832a15becc5352f387888a3d73ed Mon Sep 17 00:00:00 2001 From: shaulascr Date: Wed, 23 Apr 2025 17:12:45 +0700 Subject: [PATCH 3/7] 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 @@ + + + +