Skip to content

Commit c50bf5b

Browse files
prateekmediaclaude
andcommitted
feat: enhanced widget system with HEIC support and performance optimizations
Widget Infrastructure: - Add enhancedWidgetImage feature flag for internal users - Implement widget-type-specific cancellation mechanism - Add request generation tracking to skip outdated operations - Add circuit breaker protection (5 consecutive failures trigger 5-min cooldown) - Implement debouncing for rapid consecutive widget sync calls Image Processing: - Add HEIC/HEIF support with automatic conversion to JPEG - Increase widget image size from 512px to 1280px for better quality - Improve image quality from 70% to 100% for optimal display - Process images in isolate to avoid UI blocking - Add proper EXIF orientation handling Performance & Caching: - Optimize hash calculation using collection map for O(1) lookup - Implement multi-level caching strategy (memory, disk, widget-specific) - Add failed file tracking to avoid retrying problematic files - Reduce max attempts from 10x to 3x the limit - Clear widget cache on logout Time-based Refresh: - Implement 6-hour refresh interval for enhanced widgets - Optimize to 10 image cache for internal users (vs 50 default) - Add smart refresh skipping when all images already cached - Track refresh timestamps for scheduled updates Co-authored-by: Claude <[email protected]>
1 parent 5c5c952 commit c50bf5b

File tree

11 files changed

+1087
-44
lines changed

11 files changed

+1087
-44
lines changed

mobile/apps/photos/lib/services/album_home_widget_service.dart

Lines changed: 152 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import "dart:async";
22
import 'dart:convert';
33
import "dart:math";
44

5+
import 'package:async/async.dart';
56
import "package:collection/collection.dart";
67
import 'package:crypto/crypto.dart';
78
import 'package:flutter/material.dart';
@@ -23,14 +24,21 @@ import 'package:shared_preferences/shared_preferences.dart';
2324

2425
class AlbumHomeWidgetService {
2526
// Constants
27+
static const String WIDGET_TYPE = "album"; // Identifier for this widget type
2628
static const String SELECTED_ALBUMS_KEY = "selectedAlbumsHW";
2729
static const String ALBUMS_LAST_HASH_KEY = "albumsLastHash";
30+
static const String ALBUMS_LAST_REFRESH_KEY = "albumsLastRefresh";
2831
static const String ANDROID_CLASS_NAME = "EnteAlbumsWidgetProvider";
2932
static const String IOS_CLASS_NAME = "EnteAlbumWidget";
3033
static const String ALBUMS_CHANGED_KEY = "albumsChanged.widget";
3134
static const String ALBUMS_STATUS_KEY = "albumsStatusKey.widget";
3235
static const String TOTAL_ALBUMS_KEY = "totalAlbums";
33-
static const int MAX_ALBUMS_LIMIT = 50;
36+
// Widget optimization constants (internal users only)
37+
static const int MAX_ALBUMS_LIMIT_INTERNAL =
38+
10; // Optimized for 6-hour refresh
39+
static const int MAX_ALBUMS_LIMIT_DEFAULT = 50; // Original limit
40+
static const Duration REFRESH_INTERVAL =
41+
Duration(hours: 6); // Refresh every 6 hours
3442

3543
// Singleton pattern
3644
static final AlbumHomeWidgetService instance =
@@ -41,6 +49,13 @@ class AlbumHomeWidgetService {
4149
final Logger _logger = Logger((AlbumHomeWidgetService).toString());
4250
SharedPreferences get _prefs => ServiceLocator.instance.prefs;
4351

52+
// Track the latest request generation to skip outdated operations
53+
int _requestGeneration = 0;
54+
55+
// Debounce timer to prevent rapid consecutive widget sync calls
56+
Timer? _debounceTimer;
57+
static const Duration _debounceDuration = Duration(seconds: 2);
58+
4459
// Public methods
4560
List<int>? getSelectedAlbumIds() {
4661
final selectedAlbums = _prefs.getStringList(SELECTED_ALBUMS_KEY);
@@ -61,7 +76,32 @@ class AlbumHomeWidgetService {
6176
}
6277

6378
Future<void> initAlbumHomeWidget(bool isBg) async {
79+
// Cancel any pending debounced calls
80+
_debounceTimer?.cancel();
81+
82+
// Debounce rapid consecutive calls (except for background calls)
83+
if (!isBg) {
84+
_debounceTimer = Timer(_debounceDuration, () async {
85+
await _initAlbumHomeWidgetInternal(isBg);
86+
});
87+
} else {
88+
// Background calls are executed immediately
89+
await _initAlbumHomeWidgetInternal(isBg);
90+
}
91+
}
92+
93+
Future<void> _initAlbumHomeWidgetInternal(bool isBg) async {
94+
// Increment generation for this request
95+
final currentGeneration = ++_requestGeneration;
96+
6497
await HomeWidgetService.instance.computeLock.synchronized(() async {
98+
// Skip if a newer request has already been made
99+
if (currentGeneration != _requestGeneration) {
100+
_logger.info(
101+
"Skipping outdated album widget request (gen $currentGeneration, latest $_requestGeneration)",
102+
);
103+
return;
104+
}
65105
if (await _hasAnyBlockers(isBg)) {
66106
await clearWidget();
67107
return;
@@ -72,9 +112,23 @@ class AlbumHomeWidgetService {
72112
final bool forceFetchNewAlbums = await _shouldUpdateWidgetCache();
73113

74114
if (forceFetchNewAlbums) {
75-
await _updateAlbumsWidgetCache();
76-
await setSelectionChange(false);
77-
_logger.info("Force fetch new albums complete");
115+
// Only cancel album operations, not other widget types
116+
await HomeWidgetService.instance.cancelWidgetOperation(WIDGET_TYPE);
117+
118+
// Create a cancellable operation for this album widget update
119+
final completer = CancelableCompleter<void>();
120+
HomeWidgetService.instance
121+
.setWidgetOperation(WIDGET_TYPE, completer.operation);
122+
123+
await _updateAlbumsWidgetCacheWithCancellation(completer);
124+
if (!completer.isCanceled) {
125+
await setSelectionChange(false);
126+
_logger.info("Force fetch new albums complete");
127+
}
128+
129+
if (!completer.isCompleted && !completer.isCanceled) {
130+
completer.complete();
131+
}
78132
} else {
79133
await _refreshAlbumsWidget();
80134
_logger.info("Refresh albums widget complete");
@@ -128,7 +182,8 @@ class AlbumHomeWidgetService {
128182

129183
_logger.info("Checking pending albums sync");
130184
if (await _shouldUpdateWidgetCache()) {
131-
await initAlbumHomeWidget(false);
185+
// Use internal method to bypass debouncing for scheduled checks
186+
await _initAlbumHomeWidgetInternal(false);
132187
}
133188
}
134189

@@ -208,11 +263,14 @@ class AlbumHomeWidgetService {
208263

209264
// Private methods
210265
String _calculateHash(List<int> albumIds) {
266+
if (albumIds.isEmpty) return "";
267+
268+
// Get all collections in one shot instead of individual queries
269+
final collections = CollectionsService.instance.getActiveCollections();
211270
String updationTimestamps = "";
212271

213-
// TODO: This can be done in one shot by querying the database directly
214272
for (final albumId in albumIds) {
215-
final collection = CollectionsService.instance.getCollectionByID(albumId);
273+
final collection = collections.firstWhereOrNull((c) => c.id == albumId);
216274
if (collection != null) {
217275
updationTimestamps += "$albumId:${collection.updationTime.toString()}_";
218276
}
@@ -265,6 +323,39 @@ class AlbumHomeWidgetService {
265323
return false;
266324
}
267325

326+
// Widget optimization for enhanced widget feature
327+
if (flagService.enhancedWidgetImage) {
328+
// Check if we already have all available images (less than limit)
329+
// If the last sync was successful and we had less than the limit, no need to refresh
330+
final lastStatus = getAlbumsStatus();
331+
final totalAlbums = await _getTotalAlbums();
332+
const maxLimit = MAX_ALBUMS_LIMIT_INTERNAL;
333+
334+
if (lastStatus == WidgetStatus.syncedAll &&
335+
totalAlbums != null &&
336+
totalAlbums < maxLimit) {
337+
_logger.info(
338+
"[Enhanced] Skipping refresh: already have all available images ($totalAlbums < $maxLimit)",
339+
);
340+
return false;
341+
}
342+
343+
// Check if enough time has passed for a refresh (even if content hasn't changed)
344+
final lastRefreshStr = _prefs.getString(ALBUMS_LAST_REFRESH_KEY);
345+
if (lastRefreshStr != null) {
346+
final lastRefresh = DateTime.tryParse(lastRefreshStr);
347+
if (lastRefresh != null) {
348+
final timeSinceRefresh = DateTime.now().difference(lastRefresh);
349+
if (timeSinceRefresh >= REFRESH_INTERVAL) {
350+
_logger.info(
351+
"[Enhanced] Time-based refresh triggered (last refresh: ${timeSinceRefresh.inHours} hours ago)",
352+
);
353+
return true;
354+
}
355+
}
356+
}
357+
}
358+
268359
// Check if hash has changed
269360
final currentHash = _calculateHash(selectedAlbumIds);
270361
final lastHash = getAlbumsLastHash();
@@ -343,11 +434,23 @@ class AlbumHomeWidgetService {
343434
return albumsWithFiles;
344435
}
345436

437+
Future<int?> _getTotalAlbums() async {
438+
return await HomeWidgetService.instance.getData<int>(TOTAL_ALBUMS_KEY);
439+
}
440+
346441
Future<void> _setTotalAlbums(int? total) async {
347442
await HomeWidgetService.instance.setData(TOTAL_ALBUMS_KEY, total);
348443
}
349444

350-
Future<void> _updateAlbumsWidgetCache() async {
445+
Future<void> _updateAlbumsWidgetCacheWithCancellation(
446+
CancelableCompleter completer,
447+
) async {
448+
return _updateAlbumsWidgetCache(completer);
449+
}
450+
451+
Future<void> _updateAlbumsWidgetCache([
452+
CancelableCompleter? completer,
453+
]) async {
351454
final selectedAlbumIds = await _getEffectiveSelectedAlbumIds();
352455
final albumsWithFiles = await _getAlbumsWithFiles();
353456

@@ -358,11 +461,26 @@ class AlbumHomeWidgetService {
358461

359462
final bool isWidgetPresent = await countHomeWidgets() > 0;
360463

361-
final limit = isWidgetPresent ? MAX_ALBUMS_LIMIT : 5;
362-
final maxAttempts = limit * 10;
464+
// Use optimized limits for enhanced widget feature
465+
final maxLimit = flagService.enhancedWidgetImage
466+
? MAX_ALBUMS_LIMIT_INTERNAL
467+
: MAX_ALBUMS_LIMIT_DEFAULT;
468+
final limit = isWidgetPresent ? maxLimit : 5;
469+
470+
// Record the refresh time for enhanced widget feature
471+
if (flagService.enhancedWidgetImage) {
472+
await _prefs.setString(
473+
ALBUMS_LAST_REFRESH_KEY,
474+
DateTime.now().toIso8601String(),
475+
);
476+
}
477+
final maxAttempts =
478+
limit * 3; // Reduce max attempts to avoid excessive retries
363479

364480
int renderedCount = 0;
365481
int attemptsCount = 0;
482+
// Track files that have already failed to avoid retrying them
483+
final Set<String> failedFiles = {};
366484

367485
await updateAlbumsStatus(WidgetStatus.notSynced);
368486

@@ -371,6 +489,12 @@ class AlbumHomeWidgetService {
371489
final random = Random();
372490

373491
while (renderedCount < limit && attemptsCount < maxAttempts) {
492+
// Check if operation was cancelled
493+
if (completer != null && completer.isCanceled) {
494+
_logger.info("Albums widget update cancelled during rendering");
495+
return;
496+
}
497+
374498
final randomEntry =
375499
albumsWithFilesEntries[random.nextInt(albumsWithFilesLength)];
376500

@@ -379,6 +503,15 @@ class AlbumHomeWidgetService {
379503
final randomAlbumFile = randomEntry.value.$2.elementAt(
380504
random.nextInt(randomEntry.value.$2.length),
381505
);
506+
507+
// Skip files that have already failed
508+
final fileKey =
509+
'${randomAlbumFile.uploadedFileID ?? randomAlbumFile.localID}_${randomAlbumFile.displayName}';
510+
if (failedFiles.contains(fileKey)) {
511+
attemptsCount++;
512+
continue;
513+
}
514+
382515
final albumId = randomEntry.key;
383516
final albumName = randomEntry.value.$1;
384517

@@ -395,6 +528,12 @@ class AlbumHomeWidgetService {
395528
});
396529

397530
if (renderResult != null) {
531+
// Check if cancelled before continuing
532+
if (completer != null && completer.isCanceled) {
533+
_logger.info("Albums widget update cancelled after rendering");
534+
return;
535+
}
536+
398537
// Check for blockers again before continuing
399538
if (await _hasAnyBlockers()) {
400539
await clearWidget();
@@ -412,6 +551,9 @@ class AlbumHomeWidgetService {
412551
}
413552

414553
renderedCount++;
554+
} else {
555+
// Mark this file as failed to avoid retrying it
556+
failedFiles.add(fileKey);
415557
}
416558

417559
attemptsCount++;

0 commit comments

Comments
 (0)