From 28ad7bd4e8a5a8941de93f610067d3cbdb879a50 Mon Sep 17 00:00:00 2001 From: Oleg Date: Sat, 6 Sep 2025 21:56:58 +0300 Subject: [PATCH 01/25] feat: implement wallpaper management with Room database and update UI for recent wallpapers --- .github/copilot-instructions.md | 1 + app/build.gradle | 9 +- .../baysoft/gallerywall/GalleryAppWidget.kt | 9 +- .../gallerywall/GalleryWallReceiver.kt | 91 ++++++++++++---- .../baysoft/gallerywall/GalleryWallService.kt | 10 ++ .../com/baysoft/gallerywall/MainActivity.kt | 17 +++ .../baysoft/gallerywall/data/WallpaperDao.kt | 18 +++ .../gallerywall/data/WallpaperDatabase.kt | 24 ++++ .../gallerywall/data/WallpaperEntity.kt | 11 ++ .../gallerywall/data/WallpaperRepository.kt | 21 ++++ .../baysoft/gallerywall/ui/HomeFragment.kt | 103 ++++++++++++++++++ .../gallerywall/ui/WallpaperAdapter.kt | 44 ++++++++ app/src/main/res/layout/activity_main.xml | 10 +- app/src/main/res/layout/fragment_home.xml | 41 +++++++ app/src/main/res/layout/item_wallpaper.xml | 43 ++++++++ app/src/main/res/menu/menu_home.xml | 7 ++ app/src/main/res/navigation/nav_graph.xml | 21 ++++ app/src/main/res/values/strings.xml | 4 + build.gradle | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 20 files changed, 455 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/com/baysoft/gallerywall/data/WallpaperDao.kt create mode 100644 app/src/main/java/com/baysoft/gallerywall/data/WallpaperDatabase.kt create mode 100644 app/src/main/java/com/baysoft/gallerywall/data/WallpaperEntity.kt create mode 100644 app/src/main/java/com/baysoft/gallerywall/data/WallpaperRepository.kt create mode 100644 app/src/main/java/com/baysoft/gallerywall/ui/HomeFragment.kt create mode 100644 app/src/main/java/com/baysoft/gallerywall/ui/WallpaperAdapter.kt create mode 100644 app/src/main/res/layout/fragment_home.xml create mode 100644 app/src/main/res/layout/item_wallpaper.xml create mode 100644 app/src/main/res/menu/menu_home.xml create mode 100644 app/src/main/res/navigation/nav_graph.xml diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1083c7a..2644c9f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,6 +1,7 @@ # Project overview + ## Fastlane Fastlane is used to keep metadata for playstore which includes: tilte, description, screenshots and changelog diff --git a/app/build.gradle b/app/build.gradle index b46c339..b85da62 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,7 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id 'androidx.navigation.safeargs.kotlin' + id 'kotlin-kapt' } Properties properties = new Properties() @@ -14,7 +15,7 @@ android { defaultConfig { applicationId "com.baysoft.gallerywall" - minSdk 35 + minSdk 31 compileSdk 36 versionCode 35 versionName "2.3.0" @@ -61,6 +62,12 @@ dependencies { implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' implementation 'com.github.bumptech.glide:glide:4.11.0' + implementation 'androidx.room:room-common-jvm:2.7.2' + implementation 'androidx.room:room-ktx:2.7.2' + implementation 'androidx.room:room-runtime:2.7.2' + implementation 'androidx.navigation:navigation-fragment-ktx:2.9.3' + implementation 'androidx.navigation:navigation-ui-ktx:2.9.3' + kapt 'androidx.room:room-compiler:2.7.2' androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1' annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' diff --git a/app/src/main/java/com/baysoft/gallerywall/GalleryAppWidget.kt b/app/src/main/java/com/baysoft/gallerywall/GalleryAppWidget.kt index 059d5c2..ba13a80 100644 --- a/app/src/main/java/com/baysoft/gallerywall/GalleryAppWidget.kt +++ b/app/src/main/java/com/baysoft/gallerywall/GalleryAppWidget.kt @@ -29,17 +29,18 @@ class GalleryAppWidget : AppWidgetProvider() { private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) { val views = RemoteViews(context.packageName, R.layout.app_widget) val activateIntent = Intent(context, GalleryWallReceiver::class.java) - activateIntent.action = "update" - + activateIntent.action = "com.baysoft.gallerywall.WIDGET_REFRESH" + // Use unique data URI to ensure PendingIntent is always unique + activateIntent.data = android.net.Uri.parse("gallerywall://refresh/" + System.currentTimeMillis() + "/" + appWidgetId) val activatePending = PendingIntent.getBroadcast( - context, 1, activateIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + context, appWidgetId, activateIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE ) views.setOnClickPendingIntent(R.id.v_btn_update, activatePending) val inActivity = Intent(context, MainActivity::class.java) inActivity.putExtra("key", "value1") - val activityIntent = PendingIntent.getActivity(context, 0, inActivity, PendingIntent.FLAG_IMMUTABLE) + val activityIntent = PendingIntent.getActivity(context, appWidgetId, inActivity, PendingIntent.FLAG_IMMUTABLE) views.setOnClickPendingIntent(R.id.v_btn_settings, activityIntent) appWidgetManager.updateAppWidget(appWidgetId, views) diff --git a/app/src/main/java/com/baysoft/gallerywall/GalleryWallReceiver.kt b/app/src/main/java/com/baysoft/gallerywall/GalleryWallReceiver.kt index 0daa129..5d80fc4 100644 --- a/app/src/main/java/com/baysoft/gallerywall/GalleryWallReceiver.kt +++ b/app/src/main/java/com/baysoft/gallerywall/GalleryWallReceiver.kt @@ -36,34 +36,77 @@ class GalleryWallReceiver : BroadcastReceiver() { GalleryWall.schedule(it) } } - } - - context?.let { - GalleryAppWidget.updateLoading(it) - - GlobalScope.launch { - val imageUrl = photo ?: GalleryWall.fetchImageURL(it) - - Glide.with(it).asBitmap().load(imageUrl) - .addListener(object : RequestListener { - override fun onLoadFailed( + "com.baysoft.gallerywall.WIDGET_REFRESH" -> { + context?.let { + GalleryAppWidget.updateLoading(it) + GlobalScope.launch { + val imageUrl = photo ?: GalleryWall.fetchImageURL(it) + Glide.with(it).asBitmap().load(imageUrl) + .addListener(object : RequestListener { + override fun onLoadFailed( e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean - ): Boolean { - GalleryAppWidget.updateLoaded(it) - return false - } - - override fun onResourceReady( + ): Boolean { + // Update all widget instances to ensure state is reset + GalleryAppWidget.updateLoaded(it) + return false + } + override fun onResourceReady( + resource: Bitmap?, model: Any?, target: Target?, + dataSource: DataSource?, isFirstResource: Boolean + ): Boolean { + // change wallpaper + GalleryWall.updateWallpaper(it, resource) + GalleryAppWidget.updateLoaded(it) + // Save to recents and notify UI + GlobalScope.launch { + val db = com.baysoft.gallerywall.data.WallpaperDatabase.getInstance(it) + val repo = com.baysoft.gallerywall.data.WallpaperRepository(db.wallpaperDao()) + repo.addWallpaper(imageUrl) + val updateIntent = Intent("com.baysoft.gallerywall.WALLPAPER_SET") + it.sendBroadcast(updateIntent) + } + return false + } + }).submit() + } + } + } + else -> { + context?.let { + // Always reset widget state after any wallpaper update + GlobalScope.launch { + val imageUrl = photo ?: GalleryWall.fetchImageURL(it) + Glide.with(it).asBitmap().load(imageUrl) + .addListener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, model: Any?, + target: Target?, isFirstResource: Boolean + ): Boolean { + GalleryAppWidget.updateLoaded(it) + return false + } + override fun onResourceReady( resource: Bitmap?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean - ): Boolean { - // change wallpaper - GalleryWall.updateWallpaper(it, resource) - GalleryAppWidget.updateLoaded(it) - return false - } - }).submit() + ): Boolean { + // change wallpaper + GalleryWall.updateWallpaper(it, resource) + // Always reset widget state + GalleryAppWidget.updateLoaded(it) + // Save to recents and notify UI + GlobalScope.launch { + val db = com.baysoft.gallerywall.data.WallpaperDatabase.getInstance(it) + val repo = com.baysoft.gallerywall.data.WallpaperRepository(db.wallpaperDao()) + repo.addWallpaper(imageUrl) + val updateIntent = Intent("com.baysoft.gallerywall.WALLPAPER_SET") + it.sendBroadcast(updateIntent) + } + return false + } + }).submit() + } + } } } } diff --git a/app/src/main/java/com/baysoft/gallerywall/GalleryWallService.kt b/app/src/main/java/com/baysoft/gallerywall/GalleryWallService.kt index ec23ae3..4551869 100644 --- a/app/src/main/java/com/baysoft/gallerywall/GalleryWallService.kt +++ b/app/src/main/java/com/baysoft/gallerywall/GalleryWallService.kt @@ -89,6 +89,16 @@ class GalleryWallService : JobService() { manager.notify(NOTIFICATION_ID, buildNotification(photo, resource)) } + // Save wallpaper to database as recent + GlobalScope.launch { + val db = com.baysoft.gallerywall.data.WallpaperDatabase.getInstance(context) + val repo = com.baysoft.gallerywall.data.WallpaperRepository(db.wallpaperDao()) + repo.addWallpaper(photo) + // Notify UI to update recents + val updateIntent = Intent("com.baysoft.gallerywall.WALLPAPER_SET") + context.sendBroadcast(updateIntent) + } + // change wallpaper context.sendBroadcast(GalleryWallReceiver.updateIntent(context, photo)) diff --git a/app/src/main/java/com/baysoft/gallerywall/MainActivity.kt b/app/src/main/java/com/baysoft/gallerywall/MainActivity.kt index 9bae6e6..c95d5c1 100644 --- a/app/src/main/java/com/baysoft/gallerywall/MainActivity.kt +++ b/app/src/main/java/com/baysoft/gallerywall/MainActivity.kt @@ -1,14 +1,31 @@ package com.baysoft.gallerywall + import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.setupActionBarWithNavController class MainActivity : AppCompatActivity() { + private lateinit var appBarConfiguration: AppBarConfiguration + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment + val navController = navHostFragment.navController + appBarConfiguration = AppBarConfiguration(setOf(R.id.homeFragment)) + setupActionBarWithNavController(navController, appBarConfiguration) + } + + override fun onSupportNavigateUp(): Boolean { + val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment + val navController = navHostFragment.navController + return navController.navigateUp() || super.onSupportNavigateUp() } } \ No newline at end of file diff --git a/app/src/main/java/com/baysoft/gallerywall/data/WallpaperDao.kt b/app/src/main/java/com/baysoft/gallerywall/data/WallpaperDao.kt new file mode 100644 index 0000000..34c2877 --- /dev/null +++ b/app/src/main/java/com/baysoft/gallerywall/data/WallpaperDao.kt @@ -0,0 +1,18 @@ +package com.baysoft.gallerywall.data + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface WallpaperDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(wallpaper: WallpaperEntity) + + @Query("SELECT * FROM wallpapers WHERE dateAdded >= :from ORDER BY dateAdded DESC") + suspend fun getRecentWallpapers(from: Long): List + + @Query("DELETE FROM wallpapers WHERE dateAdded < :olderThan") + suspend fun deleteOlderThan(olderThan: Long) +} diff --git a/app/src/main/java/com/baysoft/gallerywall/data/WallpaperDatabase.kt b/app/src/main/java/com/baysoft/gallerywall/data/WallpaperDatabase.kt new file mode 100644 index 0000000..8ab25d8 --- /dev/null +++ b/app/src/main/java/com/baysoft/gallerywall/data/WallpaperDatabase.kt @@ -0,0 +1,24 @@ +package com.baysoft.gallerywall.data + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [WallpaperEntity::class], version = 1) +abstract class WallpaperDatabase : RoomDatabase() { + abstract fun wallpaperDao(): WallpaperDao + + companion object { + @Volatile private var INSTANCE: WallpaperDatabase? = null + + fun getInstance(context: Context): WallpaperDatabase = + INSTANCE ?: synchronized(this) { + INSTANCE ?: Room.databaseBuilder( + context.applicationContext, + WallpaperDatabase::class.java, + "wallpaper_db" + ).build().also { INSTANCE = it } + } + } +} diff --git a/app/src/main/java/com/baysoft/gallerywall/data/WallpaperEntity.kt b/app/src/main/java/com/baysoft/gallerywall/data/WallpaperEntity.kt new file mode 100644 index 0000000..6b11137 --- /dev/null +++ b/app/src/main/java/com/baysoft/gallerywall/data/WallpaperEntity.kt @@ -0,0 +1,11 @@ +package com.baysoft.gallerywall.data + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "wallpapers") +data class WallpaperEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val imagePath: String, + val dateAdded: Long // Store as epoch millis +) diff --git a/app/src/main/java/com/baysoft/gallerywall/data/WallpaperRepository.kt b/app/src/main/java/com/baysoft/gallerywall/data/WallpaperRepository.kt new file mode 100644 index 0000000..b6412ea --- /dev/null +++ b/app/src/main/java/com/baysoft/gallerywall/data/WallpaperRepository.kt @@ -0,0 +1,21 @@ +package com.baysoft.gallerywall.data + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class WallpaperRepository(private val dao: WallpaperDao) { + suspend fun addWallpaper(imagePath: String) { + val now = System.currentTimeMillis() + dao.insert(WallpaperEntity(imagePath = imagePath, dateAdded = now)) + } + + suspend fun getRecentWallpapers(): List { + val oneMonthAgo = System.currentTimeMillis() - 30L * 24 * 60 * 60 * 1000 + return dao.getRecentWallpapers(oneMonthAgo) + } + + suspend fun cleanupOldWallpapers() { + val threeMonthsAgo = System.currentTimeMillis() - 90L * 24 * 60 * 60 * 1000 + dao.deleteOlderThan(threeMonthsAgo) + } +} diff --git a/app/src/main/java/com/baysoft/gallerywall/ui/HomeFragment.kt b/app/src/main/java/com/baysoft/gallerywall/ui/HomeFragment.kt new file mode 100644 index 0000000..1015880 --- /dev/null +++ b/app/src/main/java/com/baysoft/gallerywall/ui/HomeFragment.kt @@ -0,0 +1,103 @@ +package com.baysoft.gallerywall.ui + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.baysoft.gallerywall.R +import com.baysoft.gallerywall.data.WallpaperDatabase +import com.baysoft.gallerywall.data.WallpaperRepository +import kotlinx.coroutines.launch + +class HomeFragment : Fragment() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + override fun onCreateOptionsMenu(menu: android.view.Menu, inflater: android.view.MenuInflater) { + inflater.inflate(R.menu.menu_home, menu) + super.onCreateOptionsMenu(menu, inflater) + } + + override fun onOptionsItemSelected(item: android.view.MenuItem): Boolean { + return when (item.itemId) { + R.id.action_settings -> { + findNavController().navigate(R.id.action_homeFragment_to_settingsFragment) + true + } + else -> super.onOptionsItemSelected(item) + } + } + private lateinit var recyclerView: RecyclerView + private lateinit var adapter: WallpaperAdapter + private lateinit var repository: WallpaperRepository + private var placeholderText: View? = null + private var refreshButton: View? = null + private var wallpaperSetReceiver: BroadcastReceiver? = null + private var refreshProgress: View? = null + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_home, container, false) + recyclerView = view.findViewById(R.id.recyclerView) + recyclerView.layoutManager = GridLayoutManager(context, 2) + adapter = WallpaperAdapter() + recyclerView.adapter = adapter + placeholderText = view.findViewById(R.id.placeholderText) + refreshButton = view.findViewById(R.id.refreshButton) + refreshProgress = view.findViewById(R.id.refreshProgress) + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val db = WallpaperDatabase.getInstance(requireContext()) + repository = WallpaperRepository(db.wallpaperDao()) + refreshButton?.setOnClickListener { + // Show animation + refreshButton?.visibility = View.GONE + refreshProgress?.visibility = View.VISIBLE + // Force refresh: trigger GalleryWallReceiver (same as widget) + val intent = com.baysoft.gallerywall.GalleryWallReceiver.updateIntent(requireContext(), null) + requireContext().sendBroadcast(intent) + } + loadRecents() + + // Listen for wallpaper set broadcasts to update recents automatically + wallpaperSetReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + loadRecents() + // Hide animation + refreshButton?.visibility = View.VISIBLE + refreshProgress?.visibility = View.GONE + } + } + requireContext().registerReceiver(wallpaperSetReceiver, IntentFilter("com.baysoft.gallerywall.WALLPAPER_SET")) + } + + private fun loadRecents() { + lifecycleScope.launch { + repository.cleanupOldWallpapers() + val wallpapers = repository.getRecentWallpapers() + adapter.submitList(wallpapers) + placeholderText?.visibility = if (wallpapers.isEmpty()) View.VISIBLE else View.GONE + } + } + + override fun onDestroyView() { + super.onDestroyView() + wallpaperSetReceiver?.let { + requireContext().unregisterReceiver(it) + } + } +} diff --git a/app/src/main/java/com/baysoft/gallerywall/ui/WallpaperAdapter.kt b/app/src/main/java/com/baysoft/gallerywall/ui/WallpaperAdapter.kt new file mode 100644 index 0000000..6a8f370 --- /dev/null +++ b/app/src/main/java/com/baysoft/gallerywall/ui/WallpaperAdapter.kt @@ -0,0 +1,44 @@ +package com.baysoft.gallerywall.ui + +import com.bumptech.glide.Glide +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.baysoft.gallerywall.R +import com.baysoft.gallerywall.data.WallpaperEntity + +class WallpaperAdapter : ListAdapter(DIFF) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WallpaperViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_wallpaper, parent, false) + return WallpaperViewHolder(view) + } + + override fun onBindViewHolder(holder: WallpaperViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + class WallpaperViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val imageView: ImageView = itemView.findViewById(R.id.imageView) + private val dateText: TextView = itemView.findViewById(R.id.dateText) + fun bind(wallpaper: WallpaperEntity) { + Glide.with(imageView.context) + .load(wallpaper.imagePath) + .centerCrop() + .into(imageView) + val date = java.text.DateFormat.getDateTimeInstance().format(java.util.Date(wallpaper.dateAdded)) + dateText.text = date + } + } + + companion object { + val DIFF = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: WallpaperEntity, newItem: WallpaperEntity) = oldItem.id == newItem.id + override fun areContentsTheSame(oldItem: WallpaperEntity, newItem: WallpaperEntity) = oldItem == newItem + } + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b3149ec..2ddb735 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,7 @@ - + android:layout_height="match_parent" + app:navGraph="@navigation/nav_graph" + app:defaultNavHost="true" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 0000000..44309cc --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_wallpaper.xml b/app/src/main/res/layout/item_wallpaper.xml new file mode 100644 index 0000000..59bc092 --- /dev/null +++ b/app/src/main/res/layout/item_wallpaper.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_home.xml b/app/src/main/res/menu/menu_home.xml new file mode 100644 index 0000000..55cc1d4 --- /dev/null +++ b/app/src/main/res/menu/menu_home.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 0000000..ba7385c --- /dev/null +++ b/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 180caae..ef7d22e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,4 +22,8 @@ Deliver new wallpaper silently When new wallpaper set + + Wallpaper + No recent wallpapers found + Refresh \ No newline at end of file diff --git a/build.gradle b/build.gradle index 62cf3cf..0d799c3 100644 --- a/build.gradle +++ b/build.gradle @@ -10,8 +10,8 @@ buildscript { } plugins { - id 'com.android.application' version '8.10.0' apply false - id 'com.android.library' version '8.10.0' apply false + id 'com.android.application' version '8.13.0' apply false + id 'com.android.library' version '8.13.0' apply false id 'org.jetbrains.kotlin.android' version '2.1.0' apply false id 'androidx.navigation.safeargs.kotlin' version '2.5.3' apply false } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a7a4b15..6838fdf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sun Mar 05 20:36:06 EET 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 529dc34942a79f53eb2a5c3942bcd257b0c3488f Mon Sep 17 00:00:00 2001 From: Oleg Date: Sat, 6 Sep 2025 22:34:17 +0300 Subject: [PATCH 02/25] feat: add wallpaper notification and update wallpaper saving logic --- .../gallerywall/GalleryWallReceiver.kt | 44 +++++++++++++++++-- .../baysoft/gallerywall/GalleryWallService.kt | 9 +++- .../gallerywall/data/WallpaperEntity.kt | 2 +- .../gallerywall/data/WallpaperRepository.kt | 5 ++- .../baysoft/gallerywall/ui/HomeFragment.kt | 28 +++++++++--- .../gallerywall/ui/WallpaperAdapter.kt | 15 +++++-- app/src/main/res/layout/item_wallpaper.xml | 36 ++++++++++----- app/src/main/res/values/strings.xml | 1 + 8 files changed, 112 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/baysoft/gallerywall/GalleryWallReceiver.kt b/app/src/main/java/com/baysoft/gallerywall/GalleryWallReceiver.kt index 5d80fc4..2f1a2c5 100644 --- a/app/src/main/java/com/baysoft/gallerywall/GalleryWallReceiver.kt +++ b/app/src/main/java/com/baysoft/gallerywall/GalleryWallReceiver.kt @@ -3,7 +3,11 @@ package com.baysoft.gallerywall import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.app.NotificationManager +import android.app.NotificationChannel +import android.os.Build import android.graphics.Bitmap +import androidx.core.app.NotificationCompat import com.bumptech.glide.Glide import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException @@ -58,11 +62,36 @@ class GalleryWallReceiver : BroadcastReceiver() { // change wallpaper GalleryWall.updateWallpaper(it, resource) GalleryAppWidget.updateLoaded(it) - // Save to recents and notify UI + // Show notification + val channelId = "_gallerywall" + val notificationManager = it.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel(channelId, it.getString(R.string.app_name), NotificationManager.IMPORTANCE_DEFAULT) + notificationManager.createNotificationChannel(channel) + } + val builder = NotificationCompat.Builder(it, channelId) + .setSmallIcon(R.drawable.icon_notification) + .setContentTitle(it.getString(R.string.notification_title_set)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setOngoing(false) + .setAutoCancel(true) + resource?.let { bmp -> + builder.setLargeIcon(bmp) + builder.setStyle(NotificationCompat.BigPictureStyle().bigPicture(bmp)) + } + notificationManager.notify(1, builder.build()) + // Save bitmap to file and store file path in recents GlobalScope.launch { + val filePath = resource?.let { bmp -> + val file = java.io.File(it.filesDir, "wallpaper_${System.currentTimeMillis()}.jpg") + java.io.FileOutputStream(file).use { out -> + bmp.compress(Bitmap.CompressFormat.JPEG, 95, out) + } + file.absolutePath + } ?: imageUrl val db = com.baysoft.gallerywall.data.WallpaperDatabase.getInstance(it) val repo = com.baysoft.gallerywall.data.WallpaperRepository(db.wallpaperDao()) - repo.addWallpaper(imageUrl) + repo.addWallpaper(filePath) val updateIntent = Intent("com.baysoft.gallerywall.WALLPAPER_SET") it.sendBroadcast(updateIntent) } @@ -94,11 +123,18 @@ class GalleryWallReceiver : BroadcastReceiver() { GalleryWall.updateWallpaper(it, resource) // Always reset widget state GalleryAppWidget.updateLoaded(it) - // Save to recents and notify UI + // Save bitmap to file and store file path in recents GlobalScope.launch { + val filePath = resource?.let { bmp -> + val file = java.io.File(it.filesDir, "wallpaper_${System.currentTimeMillis()}.jpg") + java.io.FileOutputStream(file).use { out -> + bmp.compress(Bitmap.CompressFormat.JPEG, 95, out) + } + file.absolutePath + } ?: imageUrl val db = com.baysoft.gallerywall.data.WallpaperDatabase.getInstance(it) val repo = com.baysoft.gallerywall.data.WallpaperRepository(db.wallpaperDao()) - repo.addWallpaper(imageUrl) + repo.addWallpaper(filePath) val updateIntent = Intent("com.baysoft.gallerywall.WALLPAPER_SET") it.sendBroadcast(updateIntent) } diff --git a/app/src/main/java/com/baysoft/gallerywall/GalleryWallService.kt b/app/src/main/java/com/baysoft/gallerywall/GalleryWallService.kt index 4551869..3352d94 100644 --- a/app/src/main/java/com/baysoft/gallerywall/GalleryWallService.kt +++ b/app/src/main/java/com/baysoft/gallerywall/GalleryWallService.kt @@ -91,9 +91,16 @@ class GalleryWallService : JobService() { // Save wallpaper to database as recent GlobalScope.launch { + val filePath = resource?.let { bmp -> + val file = java.io.File(context.filesDir, "wallpaper_${System.currentTimeMillis()}.jpg") + java.io.FileOutputStream(file).use { out -> + bmp.compress(Bitmap.CompressFormat.JPEG, 95, out) + } + file.absolutePath + } ?: photo val db = com.baysoft.gallerywall.data.WallpaperDatabase.getInstance(context) val repo = com.baysoft.gallerywall.data.WallpaperRepository(db.wallpaperDao()) - repo.addWallpaper(photo) + repo.addWallpaper(filePath) // Notify UI to update recents val updateIntent = Intent("com.baysoft.gallerywall.WALLPAPER_SET") context.sendBroadcast(updateIntent) diff --git a/app/src/main/java/com/baysoft/gallerywall/data/WallpaperEntity.kt b/app/src/main/java/com/baysoft/gallerywall/data/WallpaperEntity.kt index 6b11137..4bd79bb 100644 --- a/app/src/main/java/com/baysoft/gallerywall/data/WallpaperEntity.kt +++ b/app/src/main/java/com/baysoft/gallerywall/data/WallpaperEntity.kt @@ -6,6 +6,6 @@ import androidx.room.PrimaryKey @Entity(tableName = "wallpapers") data class WallpaperEntity( @PrimaryKey(autoGenerate = true) val id: Long = 0, - val imagePath: String, + val filePath: String, val dateAdded: Long // Store as epoch millis ) diff --git a/app/src/main/java/com/baysoft/gallerywall/data/WallpaperRepository.kt b/app/src/main/java/com/baysoft/gallerywall/data/WallpaperRepository.kt index b6412ea..f3b05e9 100644 --- a/app/src/main/java/com/baysoft/gallerywall/data/WallpaperRepository.kt +++ b/app/src/main/java/com/baysoft/gallerywall/data/WallpaperRepository.kt @@ -3,10 +3,11 @@ package com.baysoft.gallerywall.data import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext + class WallpaperRepository(private val dao: WallpaperDao) { - suspend fun addWallpaper(imagePath: String) { + suspend fun addWallpaper(filePath: String) { val now = System.currentTimeMillis() - dao.insert(WallpaperEntity(imagePath = imagePath, dateAdded = now)) + dao.insert(WallpaperEntity(filePath = filePath, dateAdded = now)) } suspend fun getRecentWallpapers(): List { diff --git a/app/src/main/java/com/baysoft/gallerywall/ui/HomeFragment.kt b/app/src/main/java/com/baysoft/gallerywall/ui/HomeFragment.kt index 1015880..2e20bf1 100644 --- a/app/src/main/java/com/baysoft/gallerywall/ui/HomeFragment.kt +++ b/app/src/main/java/com/baysoft/gallerywall/ui/HomeFragment.kt @@ -17,6 +17,7 @@ import com.baysoft.gallerywall.R import com.baysoft.gallerywall.data.WallpaperDatabase import com.baysoft.gallerywall.data.WallpaperRepository import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class HomeFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { @@ -49,14 +50,29 @@ class HomeFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val view = inflater.inflate(R.layout.fragment_home, container, false) - recyclerView = view.findViewById(R.id.recyclerView) - recyclerView.layoutManager = GridLayoutManager(context, 2) - adapter = WallpaperAdapter() + recyclerView = view.findViewById(R.id.recyclerView) + recyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) + adapter = WallpaperAdapter { wallpaper -> + // Set wallpaper in background thread + lifecycleScope.launch(kotlinx.coroutines.Dispatchers.IO) { + try { + val context = requireContext().applicationContext + val bitmap = com.bumptech.glide.Glide.with(context) + .asBitmap() + .load(wallpaper.filePath) + .submit() + .get() + com.baysoft.gallerywall.GalleryWall.updateWallpaper(context, bitmap) + } catch (e: Exception) { + // Optionally show error + } + } + } recyclerView.adapter = adapter placeholderText = view.findViewById(R.id.placeholderText) - refreshButton = view.findViewById(R.id.refreshButton) - refreshProgress = view.findViewById(R.id.refreshProgress) - return view + refreshButton = view.findViewById(R.id.refreshButton) + refreshProgress = view.findViewById(R.id.refreshProgress) + return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/baysoft/gallerywall/ui/WallpaperAdapter.kt b/app/src/main/java/com/baysoft/gallerywall/ui/WallpaperAdapter.kt index 6a8f370..fc7b35b 100644 --- a/app/src/main/java/com/baysoft/gallerywall/ui/WallpaperAdapter.kt +++ b/app/src/main/java/com/baysoft/gallerywall/ui/WallpaperAdapter.kt @@ -12,26 +12,33 @@ import androidx.recyclerview.widget.RecyclerView import com.baysoft.gallerywall.R import com.baysoft.gallerywall.data.WallpaperEntity -class WallpaperAdapter : ListAdapter(DIFF) { + +class WallpaperAdapter( + private val onSetWallpaper: (WallpaperEntity) -> Unit +) : ListAdapter(DIFF) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WallpaperViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_wallpaper, parent, false) - return WallpaperViewHolder(view) + return WallpaperViewHolder(view, onSetWallpaper) } override fun onBindViewHolder(holder: WallpaperViewHolder, position: Int) { holder.bind(getItem(position)) } - class WallpaperViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + class WallpaperViewHolder(itemView: View, val onSetWallpaper: (WallpaperEntity) -> Unit) : RecyclerView.ViewHolder(itemView) { private val imageView: ImageView = itemView.findViewById(R.id.imageView) private val dateText: TextView = itemView.findViewById(R.id.dateText) + private val btnSetWallpaper: View? = itemView.findViewById(R.id.btnSetWallpaper) fun bind(wallpaper: WallpaperEntity) { Glide.with(imageView.context) - .load(wallpaper.imagePath) + .load(java.io.File(wallpaper.filePath)) .centerCrop() .into(imageView) val date = java.text.DateFormat.getDateTimeInstance().format(java.util.Date(wallpaper.dateAdded)) dateText.text = date + btnSetWallpaper?.setOnClickListener { + onSetWallpaper(wallpaper) + } } } diff --git a/app/src/main/res/layout/item_wallpaper.xml b/app/src/main/res/layout/item_wallpaper.xml index 59bc092..cebb970 100644 --- a/app/src/main/res/layout/item_wallpaper.xml +++ b/app/src/main/res/layout/item_wallpaper.xml @@ -26,18 +26,34 @@ android:background="@android:color/transparent" android:contentDescription="@string/wallpaper" /> - + android:orientation="horizontal" + android:gravity="end|center_vertical" + android:paddingStart="8dp" + android:paddingEnd="8dp" + android:paddingBottom="8dp"> + + + +