Skip to content

Feature: Home Screen Photo Widget#16524

Open
szyxxx wants to merge 21 commits intonextcloud:masterfrom
szyxxx:master
Open

Feature: Home Screen Photo Widget#16524
szyxxx wants to merge 21 commits intonextcloud:masterfrom
szyxxx:master

Conversation

@szyxxx
Copy link

@szyxxx szyxxx commented Feb 16, 2026

Summary

This PR adds a configurable home screen widget that displays random photos from a selected Nextcloud folder. It supports offline fallback, recursive folder search, and custom refresh intervals.

Features

  • 📷 Random Photo Display: Shows a random photo from a user-selected folder (including subfolders).
  • 🌟 High Resolution: Fetches high-quality previews (2048px) and displays them sharp (max 800px) on the widget.
  • 📂 Recursive Search: Correctly finds images inside nested directories (e.g., Memories/2023/).
  • ⚡ Offline Resilience: Caches thumbnails and retries multiple images if one fails to load, ensuring the widget never looks broken.
  • ⏱️ Configurable Interval: User can choose refresh intervals of 5, 15, 30, 60 minutes, or "Manual only".
  • ⏭️ Manual Refresh: "Next Image" button to immediately cycle to a new photo (works offline using cached images).
  • 📍 Metadata Overlay: Displays location and date (if available) with a sleek, shadow-backed text overlay.

User Interface

  • Widget: Resizable, supports rounded corners (system default).
  • Configuration: Launches standard FolderPickerActivity, followed by a native dialog for interval selection.
  • Overlay: Text is readable on any background (white text with shadow).

Testing

  1. Install app and long-press home screen to add "Nextcloud Photo".
  2. Select a folder containing images (and subfolders).
  3. Choose a refresh interval (e.g., 15 mins).
  4. Verify widget loads an image.
  5. Tap "Next" button to verify immediate refresh.
  6. Turn off data/wifi and verify widget still loads cached images.
  7. Tap the photo to open the folder in the Nextcloud app.

Screenshots

image image image image

Copilot AI review requested due to automatic review settings February 16, 2026 11:33
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a configurable home screen widget that displays random photos from a selected Nextcloud folder. The widget supports offline fallback through cached thumbnails, recursive folder search, configurable refresh intervals (5/15/30/60 minutes or manual), and displays photo metadata (location and date) as an overlay. Users can manually cycle through photos using a "Next Image" button and tap photos to open the folder in the Nextcloud app.

Changes:

  • Added photo widget infrastructure with configuration activity, provider, worker, and repository
  • Implemented high-quality image fetching (2048px server preview, 800px widget display) with multi-tier caching strategy
  • Integrated periodic and immediate background job scheduling for widget updates

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
PhotoWidgetProvider.kt Widget provider handling lifecycle events and user interactions
PhotoWidgetWorker.kt Background worker for fetching photos and updating all widget instances
PhotoWidgetRepository.kt Data layer managing widget config persistence and image retrieval with caching
PhotoWidgetConfigActivity.kt Configuration UI for folder selection and refresh interval picker
PhotoWidgetConfig.kt Data model for per-widget configuration
PhotoWidgetRepositoryTest.kt Unit tests for repository configuration management
BackgroundJobManager.kt Interface additions for photo widget job scheduling
BackgroundJobManagerImpl.kt Implementation of periodic and immediate widget update jobs
BackgroundJobFactory.kt Worker factory integration for PhotoWidgetWorker
ComponentsModule.java Dependency injection setup for widget components
AndroidManifest.xml Widget provider and configuration activity registration
widget_photo.xml Widget layout with image, metadata overlay, and next button
photo_widget_info.xml Widget metadata configuration
ic_skip_next.xml Next button icon drawable
strings.xml String resources for widget UI

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

android:layout_marginBottom="8dp"
android:contentDescription="@string/photo_widget_next_image"
android:padding="6dp"
android:scaleType="fitCenter"
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Next image" button lacks visual affordance or background, which may make it difficult to see against light-colored photos. Consider adding a semi-transparent background or using a Material Design FloatingActionButton style to ensure the button is always visible and clearly actionable.

Suggested change
android:scaleType="fitCenter"
android:scaleType="fitCenter"
android:background="#80000000"
android:tint="@android:color/white"

Copilot uses AI. Check for mistakes.
const val JOB_INTERNAL_TWO_WAY_SYNC = "internal_two_way_sync"
const val JOB_PERIODIC_PHOTO_WIDGET = "periodic_photo_widget"
const val JOB_IMMEDIATE_PHOTO_WIDGET = "immediate_photo_widget"
const val PHOTO_WIDGET_INTERVAL_MINUTES = 15L
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constant PHOTO_WIDGET_INTERVAL_MINUTES is defined but never used in the code. The default interval is already defined in PhotoWidgetConfig.DEFAULT_INTERVAL_MINUTES and is used consistently throughout the code. This constant should either be removed or used in place of the hardcoded default value in the BackgroundJobManager interface (line 176).

Suggested change
const val PHOTO_WIDGET_INTERVAL_MINUTES = 15L

Copilot uses AI. Check for mistakes.
val config = PhotoWidgetConfig(appWidgetId, folderPath, accountName, intervalMinutes)
photoWidgetRepository.saveWidgetConfig(config)

// Schedule periodic updates (or cancel if manual)
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schedulePeriodicPhotoWidgetUpdate method is called globally for all photo widgets, but each widget can have its own refresh interval configured by the user. When a new widget is added or configured with a different interval, it will replace the global periodic work schedule, potentially affecting all other widgets. Consider either: 1) using per-widget work requests with unique tags, 2) using the shortest interval among all widgets, or 3) documenting that all widgets share the same refresh schedule (last configured wins).

Suggested change
// Schedule periodic updates (or cancel if manual)
// Schedule periodic updates (or cancel if manual).
//
// NOTE: The periodic photo widget update is scheduled globally and shared
// by all photo widgets. Calling this method updates the single shared
// schedule, so the interval chosen here (for the most recently configured
// widget) will apply to every photo widget ("last configured wins").

Copilot uses AI. Check for mistakes.
Comment on lines 200 to 221
private fun scaleBitmap(bitmap: Bitmap): Bitmap {
val width = bitmap.width
val height = bitmap.height

if (width <= MAX_BITMAP_DIMENSION && height <= MAX_BITMAP_DIMENSION) {
return bitmap
}

val ratio = width.toFloat() / height.toFloat()
val newWidth: Int
val newHeight: Int

if (width > height) {
newWidth = MAX_BITMAP_DIMENSION
newHeight = (MAX_BITMAP_DIMENSION / ratio).toInt()
} else {
newHeight = MAX_BITMAP_DIMENSION
newWidth = (MAX_BITMAP_DIMENSION * ratio).toInt()
}

return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When scaling down bitmaps, the original bitmap should be recycled if a new bitmap is created to avoid memory leaks. After calling Bitmap.createScaledBitmap, check if the returned bitmap is different from the input bitmap, and if so, call bitmap.recycle() on the original. This is especially important for large images that could be frequently loaded by the widget.

Copilot uses AI. Check for mistakes.
Comment on lines 112 to 120
@Suppress("DEPRECATION")
private fun resolveLocationName(latitude: Double?, longitude: Double?): String? {
if (latitude == null || longitude == null) return null
if (latitude == 0.0 && longitude == 0.0) return null

return try {
if (!Geocoder.isPresent()) return null
val geocoder = Geocoder(context, Locale.getDefault())
val addresses = geocoder.getFromLocation(latitude, longitude, 1)
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Geocoder.getFromLocation method is deprecated in API 33+ and can block the main thread. Since this is running in a CoroutineWorker, consider using the newer getFromLocationAsync with a callback or using withContext(Dispatchers.IO) to ensure the deprecated synchronous call doesn't cause issues. Additionally, geocoding can be slow and may fail, which could impact widget refresh performance.

Copilot uses AI. Check for mistakes.
super.onReceive(context, intent)
}

override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The injection call is missing in the onUpdate lifecycle method. Based on the pattern used in DashboardWidgetProvider (see DashboardWidgetProvider.kt:28), AndroidInjection.inject(this, context) should be called before using the injected dependencies.

Suggested change
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
AndroidInjection.inject(this, context)

Copilot uses AI. Check for mistakes.
Comment on lines 50 to 65
override fun onEnabled(context: Context) {
super.onEnabled(context)
backgroundJobManager.schedulePeriodicPhotoWidgetUpdate()
}

override fun onDisabled(context: Context) {
super.onDisabled(context)
backgroundJobManager.cancelPeriodicPhotoWidgetUpdate()
}

override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
for (widgetId in appWidgetIds) {
photoWidgetRepository.deleteWidgetConfig(widgetId)
}
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The injection call is missing in lifecycle methods. Based on the pattern in DashboardWidgetProvider (see DashboardWidgetProvider.kt:44,58), AndroidInjection.inject(this, context) should be called in onEnabled, onDisabled, and onDeleted before using injected dependencies.

Copilot uses AI. Check for mistakes.
Comment on lines 53 to 60
@Test
fun `saveWidgetConfig stores folder path and account name`() {
repository.saveWidgetConfig(WIDGET_ID, FOLDER_PATH, ACCOUNT_NAME)

verify(editor).putString(eq("photo_widget_folder_path_$WIDGET_ID"), eq(FOLDER_PATH))
verify(editor).putString(eq("photo_widget_account_name_$WIDGET_ID"), eq(ACCOUNT_NAME))
verify(editor).apply()
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test method signature is inconsistent with the production code. The production saveWidgetConfig method takes a PhotoWidgetConfig object (line 69 in PhotoWidgetRepository.kt), but the test calls it with three separate parameters (widgetId, folderPath, accountName). This test will not compile and should be updated to match the production signature.

Copilot uses AI. Check for mistakes.

private fun createOpenFolderIntent(config: PhotoWidgetConfig?): Intent {
val intent = Intent(context, FileDisplayActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The createOpenFolderIntent function doesn't utilize the config parameter, which contains the folder path that should be opened. The intent should include the folder path and account information so that clicking the photo actually opens the correct folder in FileDisplayActivity, as described in the PR description's testing section.

Suggested change
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
if (config != null) {
// Pass folder path and account information so FileDisplayActivity
// can open the correct folder for this widget configuration.
intent.putExtra("folderPath", config.folderPath)
intent.putExtra("accountName", config.accountName)
}

Copilot uses AI. Check for mistakes.
Comment on lines 829 to 853
override fun schedulePeriodicPhotoWidgetUpdate(intervalMinutes: Long) {
// Manual mode: cancel any existing periodic work
if (intervalMinutes <= 0L) {
cancelPeriodicPhotoWidgetUpdate()
return
}

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

val request = periodicRequestBuilder(
jobClass = com.nextcloud.client.widget.photo.PhotoWidgetWorker::class,
jobName = JOB_PERIODIC_PHOTO_WIDGET,
intervalMins = intervalMinutes
)
.setConstraints(constraints)
.build()

workManager.enqueueUniquePeriodicWork(
JOB_PERIODIC_PHOTO_WIDGET,
ExistingPeriodicWorkPolicy.REPLACE,
request
)
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schedulePeriodicPhotoWidgetUpdate method requires network connectivity (line 837), but the PhotoWidgetRepository is designed to support offline fallback by using cached images. When users enable the widget and select a manual-only refresh interval, they may still expect cached images to rotate. Consider removing the network constraint from periodic updates or making it optional, as the repository can successfully serve cached images offline.

Copilot uses AI. Check for mistakes.
Signed-off-by: szyxxx <axeldavid1521@gmail.com>
…anagement and configuration.

Signed-off-by: szyxxx <axeldavid1521@gmail.com>
…ates, and image display.

Signed-off-by: szyxxx <axeldavid1521@gmail.com>
Signed-off-by: szyxxx <axeldavid1521@gmail.com>
Signed-off-by: szyxxx <axeldavid1521@gmail.com>
Signed-off-by: szyxxx <axeldavid1521@gmail.com>
…photos with location and refresh functionality.
Signed-off-by: szyxxx <axeldavid1521@gmail.com>
…anagement and configuration.

Signed-off-by: szyxxx <axeldavid1521@gmail.com>
…ates, and image display.

Signed-off-by: szyxxx <axeldavid1521@gmail.com>
Signed-off-by: szyxxx <axeldavid1521@gmail.com>
Signed-off-by: szyxxx <axeldavid1521@gmail.com>
Signed-off-by: szyxxx <axeldavid1521@gmail.com>
…photos with location and refresh functionality.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments