diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml
index 94567c1cd5..196f8faf59 100644
--- a/.github/workflows/static_analysis.yml
+++ b/.github/workflows/static_analysis.yml
@@ -56,6 +56,10 @@ jobs:
         run: dart format lib/ --set-exit-if-changed
         working-directory: ./mobile
 
+      - name: Run dart custom_lint
+        run: dart run custom_lint
+        working-directory: ./mobile
+
       # Enable after riverpod generator migration is completed
       # - name: Run dart custom lint
       #   run: dart run custom_lint
diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml
index fe5729fc60..3286a9a8f6 100644
--- a/mobile/analysis_options.yaml
+++ b/mobile/analysis_options.yaml
@@ -36,8 +36,31 @@ analyzer:
     - openapi/**
     - lib/generated_plugin_registrant.dart
 
-plugins:
-  - custom_lint
+  plugins:
+    - custom_lint
+
+custom_lint:
+  debug: true
+  rules:
+    - avoid_build_context_in_providers: false
+    - avoid_public_notifier_properties: false
+    - avoid_manual_providers_as_generated_provider_dependency: false
+    - unsupported_provider_value: false
+    - photo_manager:
+      exclude:
+        # required / wanted
+        - album_media.repository.dart
+        - asset_media.repository.dart
+        - file_media.repository.dart
+          # acceptable exceptions for the time being
+        - asset.entity.dart # to provide local AssetEntity for now
+        - immich_local_image_provider.dart # accesses thumbnails via PhotoManager
+        - immich_local_thumbnail_provider.dart # accesses thumbnails via PhotoManager
+          # refactor to make the providers and services testable
+        - backup.provider.dart # uses only PMProgressHandler
+        - manual_upload.provider.dart # uses only PMProgressHandler
+        - background.service.dart # uses only PMProgressHandler
+        - backup.service.dart # uses only PMProgressHandler
 
 dart_code_metrics:
   metrics:
diff --git a/mobile/immich_lint/analysis_options.yaml b/mobile/immich_lint/analysis_options.yaml
new file mode 100644
index 0000000000..572dd239d0
--- /dev/null
+++ b/mobile/immich_lint/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:lints/recommended.yaml
diff --git a/mobile/immich_lint/lib/immich_mobile_immich_lint.dart b/mobile/immich_lint/lib/immich_mobile_immich_lint.dart
new file mode 100644
index 0000000000..31922ecc24
--- /dev/null
+++ b/mobile/immich_lint/lib/immich_mobile_immich_lint.dart
@@ -0,0 +1,49 @@
+import 'dart:collection';
+
+import 'package:analyzer/error/listener.dart';
+import 'package:analyzer/error/error.dart' show ErrorSeverity;
+import 'package:custom_lint_builder/custom_lint_builder.dart';
+
+PluginBase createPlugin() => ImmichLinter();
+
+class ImmichLinter extends PluginBase {
+  @override
+  List<LintRule> getLintRules(CustomLintConfigs configs) => [
+        PhotoManagerRule(configs.rules[PhotoManagerRule._code.name]),
+      ];
+}
+
+class PhotoManagerRule extends DartLintRule {
+  PhotoManagerRule(LintOptions? options) : super(code: _code) {
+    final excludeOption = options?.json["exclude"];
+    if (excludeOption is String) {
+      _excludePaths.add(excludeOption);
+    } else if (excludeOption is List) {
+      _excludePaths.addAll(excludeOption.map((option) => option));
+    }
+  }
+
+  final Set<String> _excludePaths = HashSet();
+
+  static const _code = LintCode(
+    name: 'photo_manager',
+    problemMessage:
+        'photo_manager library must only be used in MediaRepository',
+    errorSeverity: ErrorSeverity.WARNING,
+  );
+
+  @override
+  void run(
+    CustomLintResolver resolver,
+    ErrorReporter reporter,
+    CustomLintContext context,
+  ) {
+    if (_excludePaths.contains(resolver.source.shortName)) return;
+
+    context.registry.addImportDirective((node) {
+      if (node.uri.stringValue?.startsWith("package:photo_manager") == true) {
+        reporter.atNode(node, code);
+      }
+    });
+  }
+}
diff --git a/mobile/immich_lint/pubspec.lock b/mobile/immich_lint/pubspec.lock
new file mode 100644
index 0000000000..83bb229e82
--- /dev/null
+++ b/mobile/immich_lint/pubspec.lock
@@ -0,0 +1,370 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  _fe_analyzer_shared:
+    dependency: transitive
+    description:
+      name: _fe_analyzer_shared
+      sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
+      url: "https://pub.dev"
+    source: hosted
+    version: "73.0.0"
+  _macros:
+    dependency: transitive
+    description: dart
+    source: sdk
+    version: "0.3.2"
+  analyzer:
+    dependency: "direct main"
+    description:
+      name: analyzer
+      sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.8.0"
+  analyzer_plugin:
+    dependency: "direct main"
+    description:
+      name: analyzer_plugin
+      sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.11.3"
+  args:
+    dependency: transitive
+    description:
+      name: args
+      sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.5.0"
+  async:
+    dependency: transitive
+    description:
+      name: async
+      sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.11.0"
+  boolean_selector:
+    dependency: transitive
+    description:
+      name: boolean_selector
+      sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
+  checked_yaml:
+    dependency: transitive
+    description:
+      name: checked_yaml
+      sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.3"
+  ci:
+    dependency: transitive
+    description:
+      name: ci
+      sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.1.0"
+  cli_util:
+    dependency: transitive
+    description:
+      name: cli_util
+      sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.4.1"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.19.0"
+  convert:
+    dependency: transitive
+    description:
+      name: convert
+      sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.1"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.5"
+  custom_lint:
+    dependency: transitive
+    description:
+      name: custom_lint
+      sha256: "6e1ec47427ca968f22bce734d00028ae7084361999b41673291138945c5baca0"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.7"
+  custom_lint_builder:
+    dependency: "direct main"
+    description:
+      name: custom_lint_builder
+      sha256: ba2f90fff4eff71d202d097eb14b14f87087eaaef742e956208c0eb9d3a40a21
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.7"
+  custom_lint_core:
+    dependency: transitive
+    description:
+      name: custom_lint_core
+      sha256: "4ddbbdaa774265de44c97054dcec058a83d9081d071785ece601e348c18c267d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.5"
+  dart_style:
+    dependency: transitive
+    description:
+      name: dart_style
+      sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.7"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.0"
+  fixnum:
+    dependency: transitive
+    description:
+      name: fixnum
+      sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
+  freezed_annotation:
+    dependency: transitive
+    description:
+      name: freezed_annotation
+      sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.4"
+  glob:
+    dependency: transitive
+    description:
+      name: glob
+      sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  hotreloader:
+    dependency: transitive
+    description:
+      name: hotreloader
+      sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.2.0"
+  json_annotation:
+    dependency: transitive
+    description:
+      name: json_annotation
+      sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.9.0"
+  lints:
+    dependency: "direct dev"
+    description:
+      name: lints
+      sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.0"
+  logging:
+    dependency: transitive
+    description:
+      name: logging
+      sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.0"
+  macros:
+    dependency: transitive
+    description:
+      name: macros
+      sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.1.2-main.4"
+  matcher:
+    dependency: transitive
+    description:
+      name: matcher
+      sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.12.16+1"
+  meta:
+    dependency: transitive
+    description:
+      name: meta
+      sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.15.0"
+  package_config:
+    dependency: transitive
+    description:
+      name: package_config
+      sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.0"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.9.0"
+  pub_semver:
+    dependency: transitive
+    description:
+      name: pub_semver
+      sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.4"
+  pubspec_parse:
+    dependency: transitive
+    description:
+      name: pubspec_parse
+      sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  rxdart:
+    dependency: transitive
+    description:
+      name: rxdart
+      sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.28.0"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.10.0"
+  sprintf:
+    dependency: transitive
+    description:
+      name: sprintf
+      sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.0"
+  stack_trace:
+    dependency: transitive
+    description:
+      name: stack_trace
+      sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.11.1"
+  stream_channel:
+    dependency: transitive
+    description:
+      name: stream_channel
+      sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  stream_transform:
+    dependency: transitive
+    description:
+      name: stream_transform
+      sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.0"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.1"
+  test_api:
+    dependency: transitive
+    description:
+      name: test_api
+      sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.7.3"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.2"
+  uuid:
+    dependency: transitive
+    description:
+      name: uuid
+      sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.5.0"
+  vm_service:
+    dependency: transitive
+    description:
+      name: vm_service
+      sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "14.2.5"
+  watcher:
+    dependency: transitive
+    description:
+      name: watcher
+      sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
+  yaml:
+    dependency: transitive
+    description:
+      name: yaml
+      sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.2"
+sdks:
+  dart: ">=3.4.0 <4.0.0"
diff --git a/mobile/immich_lint/pubspec.yaml b/mobile/immich_lint/pubspec.yaml
new file mode 100644
index 0000000000..e10c665c57
--- /dev/null
+++ b/mobile/immich_lint/pubspec.yaml
@@ -0,0 +1,13 @@
+name: immich_mobile_immich_lint
+publish_to: none
+
+environment:
+  sdk: '>=3.0.0 <4.0.0'
+
+dependencies:
+  analyzer: ^6.8.0
+  analyzer_plugin: ^0.11.3
+  custom_lint_builder: ^0.6.4
+
+dev_dependencies:
+  lints: ^4.0.0
diff --git a/mobile/lib/entities/album.entity.dart b/mobile/lib/entities/album.entity.dart
index b20cec97c3..1914336cf7 100644
--- a/mobile/lib/entities/album.entity.dart
+++ b/mobile/lib/entities/album.entity.dart
@@ -1,11 +1,9 @@
 import 'package:flutter/foundation.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
 import 'package:immich_mobile/utils/datetime_comparison.dart';
 import 'package:isar/isar.dart';
 import 'package:openapi/api.dart';
-import 'package:photo_manager/photo_manager.dart';
 
 part 'album.entity.g.dart';
 
@@ -43,6 +41,9 @@ class Album {
   final IsarLinks<User> sharedUsers = IsarLinks<User>();
   final IsarLinks<Asset> assets = IsarLinks<Asset>();
 
+  @ignore
+  bool isAll = false;
+
   @ignore
   bool get isRemote => remoteId != null;
 
@@ -70,6 +71,9 @@ class Album {
     return name.join(' ');
   }
 
+  @ignore
+  String get eTagKeyAssetCount => "device-album-$localId-asset-count";
+
   @override
   bool operator ==(other) {
     if (other is! Album) return false;
@@ -112,19 +116,6 @@ class Album {
       sharedUsers.length.hashCode ^
       assets.length.hashCode;
 
-  static Album local(AssetPathEntity ape) {
-    final Album a = Album(
-      name: ape.name,
-      createdAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
-      modifiedAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
-      shared: false,
-      activityEnabled: false,
-    );
-    a.owner.value = Store.get(StoreKey.currentUser);
-    a.localId = ape.id;
-    return a;
-  }
-
   static Future<Album> remote(AlbumResponseDto dto) async {
     final Isar db = Isar.getInstance()!;
     final Album a = Album(
@@ -177,7 +168,3 @@ extension AssetsHelper on IsarCollection<Album> {
 extension AlbumResponseDtoHelper on AlbumResponseDto {
   List<Asset> getAssets() => assets.map(Asset.remote).toList();
 }
-
-extension AssetPathEntityHelper on AssetPathEntity {
-  String get eTagKeyAssetCount => "device-album-$id-asset-count";
-}
diff --git a/mobile/lib/entities/asset.entity.dart b/mobile/lib/entities/asset.entity.dart
index 97e10b3d20..df902ca995 100644
--- a/mobile/lib/entities/asset.entity.dart
+++ b/mobile/lib/entities/asset.entity.dart
@@ -1,11 +1,10 @@
 import 'dart:convert';
 
 import 'package:immich_mobile/entities/exif_info.entity.dart';
-import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/utils/hash.dart';
 import 'package:isar/isar.dart';
 import 'package:openapi/api.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show AssetEntity;
 import 'package:immich_mobile/extensions/string_extensions.dart';
 import 'package:path/path.dart' as p;
 
@@ -42,33 +41,6 @@ class Asset {
         stackId = remote.stack?.id,
         thumbhash = remote.thumbhash;
 
-  Asset.local(AssetEntity local, List<int> hash)
-      : localId = local.id,
-        checksum = base64.encode(hash),
-        durationInSeconds = local.duration,
-        type = AssetType.values[local.typeInt],
-        height = local.height,
-        width = local.width,
-        fileName = local.title!,
-        ownerId = Store.get(StoreKey.currentUser).isarId,
-        fileModifiedAt = local.modifiedDateTime,
-        updatedAt = local.modifiedDateTime,
-        isFavorite = local.isFavorite,
-        isArchived = false,
-        isTrashed = false,
-        isOffline = false,
-        stackCount = 0,
-        fileCreatedAt = local.createDateTime {
-    if (fileCreatedAt.year == 1970) {
-      fileCreatedAt = fileModifiedAt;
-    }
-    if (local.latitude != null) {
-      exifInfo = ExifInfo(lat: local.latitude, long: local.longitude);
-    }
-    _local = local;
-    assert(hash.length == 20, "invalid SHA1 hash");
-  }
-
   Asset({
     this.id = Isar.autoIncrement,
     required this.checksum,
@@ -115,6 +87,8 @@ class Asset {
     return _local;
   }
 
+  set local(AssetEntity? assetEntity) => _local = assetEntity;
+
   Id id = Isar.autoIncrement;
 
   /// stores the raw SHA1 bytes as a base64 String
@@ -210,6 +184,10 @@ class Asset {
   @ignore
   Duration get duration => Duration(seconds: durationInSeconds);
 
+  // ignore: invalid_annotation_target
+  @ignore
+  set byteHash(List<int> hash) => checksum = base64.encode(hash);
+
   @override
   bool operator ==(other) {
     if (other is! Asset) return false;
diff --git a/mobile/lib/interfaces/album_media.interface.dart b/mobile/lib/interfaces/album_media.interface.dart
new file mode 100644
index 0000000000..fd5f3c8af1
--- /dev/null
+++ b/mobile/lib/interfaces/album_media.interface.dart
@@ -0,0 +1,21 @@
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+
+abstract interface class IAlbumMediaRepository {
+  Future<List<Album>> getAll();
+
+  Future<List<String>> getAssetIds(String albumId);
+
+  Future<int> getAssetCount(String albumId);
+
+  Future<List<Asset>> getAssets(
+    String albumId, {
+    int start = 0,
+    int end = 0x7fffffffffffffff,
+    DateTime? modifiedFrom,
+    DateTime? modifiedUntil,
+    bool orderByModificationDate = false,
+  });
+
+  Future<Album> get(String id);
+}
diff --git a/mobile/lib/interfaces/asset_media.interface.dart b/mobile/lib/interfaces/asset_media.interface.dart
new file mode 100644
index 0000000000..f89a238dd4
--- /dev/null
+++ b/mobile/lib/interfaces/asset_media.interface.dart
@@ -0,0 +1,7 @@
+import 'package:immich_mobile/entities/asset.entity.dart';
+
+abstract interface class IAssetMediaRepository {
+  Future<List<String>> deleteAll(List<String> ids);
+
+  Future<Asset?> get(String id);
+}
diff --git a/mobile/lib/interfaces/file_media.interface.dart b/mobile/lib/interfaces/file_media.interface.dart
new file mode 100644
index 0000000000..c898183d79
--- /dev/null
+++ b/mobile/lib/interfaces/file_media.interface.dart
@@ -0,0 +1,30 @@
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:immich_mobile/entities/asset.entity.dart';
+
+abstract interface class IFileMediaRepository {
+  Future<Asset?> saveImage(
+    Uint8List data, {
+    required String title,
+    String? relativePath,
+  });
+
+  Future<Asset?> saveVideo(
+    File file, {
+    required String title,
+    String? relativePath,
+  });
+
+  Future<Asset?> saveLivePhoto({
+    required File image,
+    required File video,
+    required String title,
+  });
+
+  Future<void> clearFileCache();
+
+  Future<void> enableBackgroundAccess();
+
+  Future<void> requestExtendedPermissions();
+}
diff --git a/mobile/lib/models/backup/available_album.model.dart b/mobile/lib/models/backup/available_album.model.dart
index 0b428eea0f..59c57582ce 100644
--- a/mobile/lib/models/backup/available_album.model.dart
+++ b/mobile/lib/models/backup/available_album.model.dart
@@ -1,45 +1,47 @@
 import 'dart:typed_data';
 
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
 
 class AvailableAlbum {
-  final AssetPathEntity albumEntity;
+  final Album album;
+  final int assetCount;
   final DateTime? lastBackup;
   AvailableAlbum({
-    required this.albumEntity,
+    required this.album,
+    required this.assetCount,
     this.lastBackup,
   });
 
   AvailableAlbum copyWith({
-    AssetPathEntity? albumEntity,
+    Album? album,
+    int? assetCount,
     DateTime? lastBackup,
     Uint8List? thumbnailData,
   }) {
     return AvailableAlbum(
-      albumEntity: albumEntity ?? this.albumEntity,
+      album: album ?? this.album,
+      assetCount: assetCount ?? this.assetCount,
       lastBackup: lastBackup ?? this.lastBackup,
     );
   }
 
-  String get name => albumEntity.name;
+  String get name => album.name;
 
-  Future<int> get assetCount => albumEntity.assetCountAsync;
+  String get id => album.localId!;
 
-  String get id => albumEntity.id;
-
-  bool get isAll => albumEntity.isAll;
+  bool get isAll => album.isAll;
 
   @override
   String toString() =>
-      'AvailableAlbum(albumEntity: $albumEntity, lastBackup: $lastBackup)';
+      'AvailableAlbum(albumEntity: $album, lastBackup: $lastBackup)';
 
   @override
   bool operator ==(Object other) {
     if (identical(this, other)) return true;
 
-    return other is AvailableAlbum && other.albumEntity == albumEntity;
+    return other is AvailableAlbum && other.album == album;
   }
 
   @override
-  int get hashCode => albumEntity.hashCode;
+  int get hashCode => album.hashCode;
 }
diff --git a/mobile/lib/models/backup/backup_candidate.model.dart b/mobile/lib/models/backup/backup_candidate.model.dart
index 5ef1516745..01c257dc05 100644
--- a/mobile/lib/models/backup/backup_candidate.model.dart
+++ b/mobile/lib/models/backup/backup_candidate.model.dart
@@ -1,9 +1,9 @@
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
 
 class BackupCandidate {
   BackupCandidate({required this.asset, required this.albumNames});
 
-  AssetEntity asset;
+  Asset asset;
   List<String> albumNames;
 
   @override
diff --git a/mobile/lib/models/backup/error_upload_asset.model.dart b/mobile/lib/models/backup/error_upload_asset.model.dart
index b63592eda8..38f241e748 100644
--- a/mobile/lib/models/backup/error_upload_asset.model.dart
+++ b/mobile/lib/models/backup/error_upload_asset.model.dart
@@ -1,11 +1,11 @@
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
 
 class ErrorUploadAsset {
   final String id;
   final DateTime fileCreatedAt;
   final String fileName;
   final String fileType;
-  final AssetEntity asset;
+  final Asset asset;
   final String errorMessage;
 
   const ErrorUploadAsset({
@@ -22,7 +22,7 @@ class ErrorUploadAsset {
     DateTime? fileCreatedAt,
     String? fileName,
     String? fileType,
-    AssetEntity? asset,
+    Asset? asset,
     String? errorMessage,
   }) {
     return ErrorUploadAsset(
diff --git a/mobile/lib/pages/backup/album_preview.page.dart b/mobile/lib/pages/backup/album_preview.page.dart
index 5cb5d418a0..b9fed41305 100644
--- a/mobile/lib/pages/backup/album_preview.page.dart
+++ b/mobile/lib/pages/backup/album_preview.page.dart
@@ -1,28 +1,27 @@
-import 'dart:typed_data';
-
 import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/theme_extensions.dart';
-import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
 
 @RoutePage()
 class AlbumPreviewPage extends HookConsumerWidget {
-  final AssetPathEntity album;
+  final Album album;
   const AlbumPreviewPage({super.key, required this.album});
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final assets = useState<List<AssetEntity>>([]);
+    final assets = useState<List<Asset>>([]);
 
     getAssetsInAlbum() async {
-      assets.value = await album.getAssetListRange(
-        start: 0,
-        end: await album.assetCountAsync,
-      );
+      assets.value = await ref
+          .read(albumMediaRepositoryProvider)
+          .getAssets(album.localId!);
     }
 
     useEffect(
@@ -68,30 +67,10 @@ class AlbumPreviewPage extends HookConsumerWidget {
         ),
         itemCount: assets.value.length,
         itemBuilder: (context, index) {
-          Future<Uint8List?> thumbData =
-              assets.value[index].thumbnailDataWithSize(
-            const ThumbnailSize(200, 200),
-            quality: 50,
-          );
-
-          return FutureBuilder<Uint8List?>(
-            future: thumbData,
-            builder: ((context, snapshot) {
-              if (snapshot.hasData && snapshot.data != null) {
-                return Image.memory(
-                  snapshot.data!,
-                  width: 100,
-                  height: 100,
-                  fit: BoxFit.cover,
-                );
-              }
-
-              return const SizedBox(
-                width: 100,
-                height: 100,
-                child: ImmichLoadingIndicator(),
-              );
-            }),
+          return ImmichThumbnail(
+            asset: assets.value[index],
+            width: 100,
+            height: 100,
           );
         },
       ),
diff --git a/mobile/lib/pages/backup/failed_backup_status.page.dart b/mobile/lib/pages/backup/failed_backup_status.page.dart
index 1c6d3a7aad..551555d75e 100644
--- a/mobile/lib/pages/backup/failed_backup_status.page.dart
+++ b/mobile/lib/pages/backup/failed_backup_status.page.dart
@@ -3,9 +3,8 @@ import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
+import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart';
 import 'package:intl/intl.dart';
-import 'package:photo_manager/photo_manager.dart';
-import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
 
 @RoutePage()
 class FailedBackupStatusPage extends HookConsumerWidget {
@@ -70,11 +69,10 @@ class FailedBackupStatusPage extends HookConsumerWidget {
                       clipBehavior: Clip.hardEdge,
                       child: Image(
                         fit: BoxFit.cover,
-                        image: AssetEntityImageProvider(
-                          errorAsset.asset,
-                          isOriginal: false,
-                          thumbnailSize: const ThumbnailSize.square(512),
-                          thumbnailFormat: ThumbnailFormat.jpeg,
+                        image: ImmichLocalThumbnailProvider(
+                          asset: errorAsset.asset,
+                          height: 512,
+                          width: 512,
                         ),
                       ),
                     ),
diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart
index c81e84877b..5c0c185dbc 100644
--- a/mobile/lib/pages/editing/edit.page.dart
+++ b/mobile/lib/pages/editing/edit.page.dart
@@ -8,11 +8,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
 import 'package:immich_mobile/widgets/common/immich_image.dart';
 import 'package:immich_mobile/widgets/common/immich_toast.dart';
 import 'package:auto_route/auto_route.dart';
 import 'package:immich_mobile/routing/router.dart';
-import 'package:photo_manager/photo_manager.dart';
 import 'package:immich_mobile/providers/album/album.provider.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:path/path.dart' as p;
@@ -67,10 +67,10 @@ class EditImagePage extends ConsumerWidget {
   ) async {
     try {
       final Uint8List imageData = await _imageToUint8List(image);
-      await PhotoManager.editor.saveImage(
-        imageData,
-        title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
-      );
+      await ref.read(fileMediaRepositoryProvider).saveImage(
+            imageData,
+            title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
+          );
       await ref.read(albumProvider.notifier).getDeviceAlbums();
       Navigator.of(context).popUntil((route) => route.isFirst);
       ImmichToast.show(
diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart
index 3c1a5ecc01..a2c3987aa8 100644
--- a/mobile/lib/providers/asset.provider.dart
+++ b/mobile/lib/providers/asset.provider.dart
@@ -1,6 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/providers/memory.provider.dart';
+import 'package:immich_mobile/repositories/asset_media.repository.dart';
 import 'package:immich_mobile/services/album.service.dart';
 import 'package:immich_mobile/entities/exif_info.entity.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
@@ -15,7 +16,6 @@ import 'package:immich_mobile/utils/db.dart';
 import 'package:immich_mobile/utils/renderlist_generator.dart';
 import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
-import 'package:photo_manager/photo_manager.dart';
 
 class AssetNotifier extends StateNotifier<bool> {
   final AssetService _assetService;
@@ -257,7 +257,7 @@ class AssetNotifier extends StateNotifier<bool> {
     // Delete asset from device
     if (local.isNotEmpty) {
       try {
-        return await PhotoManager.editor.deleteWithIds(local);
+        return await _ref.read(assetMediaRepositoryProvider).deleteAll(local);
       } catch (e, stack) {
         log.severe("Failed to delete asset from device", e, stack);
       }
diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart
index 02f1f07904..9329f9b1f7 100644
--- a/mobile/lib/providers/backup/backup.provider.dart
+++ b/mobile/lib/providers/backup/backup.provider.dart
@@ -5,6 +5,9 @@ import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
 import 'package:immich_mobile/models/backup/available_album.model.dart';
 import 'package:immich_mobile/entities/backup_album.entity.dart';
 import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
@@ -13,6 +16,8 @@ import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
 import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
 import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
 import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
 import 'package:immich_mobile/services/background.service.dart';
 import 'package:immich_mobile/services/backup.service.dart';
 import 'package:immich_mobile/models/authentication/authentication_state.model.dart';
@@ -28,7 +33,7 @@ import 'package:immich_mobile/utils/diff.dart';
 import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
 import 'package:permission_handler/permission_handler.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
 
 class BackupNotifier extends StateNotifier<BackUpState> {
   BackupNotifier(
@@ -38,6 +43,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     this._backgroundService,
     this._galleryPermissionNotifier,
     this._db,
+    this._albumMediaRepository,
+    this._fileMediaRepository,
     this.ref,
   ) : super(
           BackUpState(
@@ -86,6 +93,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
   final BackgroundService _backgroundService;
   final GalleryPermissionNotifier _galleryPermissionNotifier;
   final Isar _db;
+  final IAlbumMediaRepository _albumMediaRepository;
+  final IFileMediaRepository _fileMediaRepository;
   final Ref ref;
 
   ///
@@ -224,22 +233,24 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     Stopwatch stopwatch = Stopwatch()..start();
     // Get all albums on the device
     List<AvailableAlbum> availableAlbums = [];
-    List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
-      hasAll: true,
-      type: RequestType.common,
-    );
+    List<Album> albums = await _albumMediaRepository.getAll();
 
     // Map of id -> album for quick album lookup later on.
-    Map<String, AssetPathEntity> albumMap = {};
+    Map<String, Album> albumMap = {};
 
     log.info('Found ${albums.length} local albums');
 
-    for (AssetPathEntity album in albums) {
-      AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
+    for (Album album in albums) {
+      AvailableAlbum availableAlbum = AvailableAlbum(
+        album: album,
+        assetCount: await ref
+            .read(albumMediaRepositoryProvider)
+            .getAssetCount(album.localId!),
+      );
 
       availableAlbums.add(availableAlbum);
 
-      albumMap[album.id] = album;
+      albumMap[album.localId!] = album;
     }
     state = state.copyWith(availableAlbums: availableAlbums);
 
@@ -248,14 +259,18 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     final List<BackupAlbum> selectedBackupAlbums =
         await _backupService.selectedAlbumsQuery().findAll();
 
-    // Generate AssetPathEntity from id to add to local state
     final Set<AvailableAlbum> selectedAlbums = {};
     for (final BackupAlbum ba in selectedBackupAlbums) {
       final albumAsset = albumMap[ba.id];
 
       if (albumAsset != null) {
         selectedAlbums.add(
-          AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
+          AvailableAlbum(
+            album: albumAsset,
+            assetCount:
+                await _albumMediaRepository.getAssetCount(albumAsset.localId!),
+            lastBackup: ba.lastBackup,
+          ),
         );
       } else {
         log.severe('Selected album not found');
@@ -268,7 +283,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 
       if (albumAsset != null) {
         excludedAlbums.add(
-          AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
+          AvailableAlbum(
+            album: albumAsset,
+            assetCount: await ref
+                .read(albumMediaRepositoryProvider)
+                .getAssetCount(albumAsset.localId!),
+            lastBackup: ba.lastBackup,
+          ),
         );
       } else {
         log.severe('Excluded album not found');
@@ -297,23 +318,24 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     final Set<BackupCandidate> assetsFromExcludedAlbums = {};
 
     for (final album in state.selectedBackupAlbums) {
-      final assetCount = await album.albumEntity.assetCountAsync;
+      final assetCount = await ref
+          .read(albumMediaRepositoryProvider)
+          .getAssetCount(album.album.localId!);
 
       if (assetCount == 0) {
         continue;
       }
 
-      final assets = await album.albumEntity.getAssetListRange(
-        start: 0,
-        end: assetCount,
-      );
+      final assets = await ref
+          .read(albumMediaRepositoryProvider)
+          .getAssets(album.album.localId!);
 
       // Add album's name to the asset info
       for (final asset in assets) {
         List<String> albumNames = [album.name];
 
         final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull(
-          (a) => a.asset.id == asset.id,
+          (a) => a.asset.localId == asset.localId,
         );
 
         if (existingAsset != null) {
@@ -331,16 +353,17 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     }
 
     for (final album in state.excludedBackupAlbums) {
-      final assetCount = await album.albumEntity.assetCountAsync;
+      final assetCount = await ref
+          .read(albumMediaRepositoryProvider)
+          .getAssetCount(album.album.localId!);
 
       if (assetCount == 0) {
         continue;
       }
 
-      final assets = await album.albumEntity.getAssetListRange(
-        start: 0,
-        end: assetCount,
-      );
+      final assets = await ref
+          .read(albumMediaRepositoryProvider)
+          .getAssets(album.album.localId!);
 
       for (final asset in assets) {
         assetsFromExcludedAlbums.add(
@@ -360,14 +383,14 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 
     // Find asset that were backup from selected albums
     final Set<String> selectedAlbumsBackupAssets =
-        Set.from(allUniqueAssets.map((e) => e.asset.id));
+        Set.from(allUniqueAssets.map((e) => e.asset.localId));
 
     selectedAlbumsBackupAssets
         .removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
 
     // Remove duplicated asset from all unique assets
     allUniqueAssets.removeWhere(
-      (candidate) => duplicatedAssetIds.contains(candidate.asset.id),
+      (candidate) => duplicatedAssetIds.contains(candidate.asset.localId),
     );
 
     if (allUniqueAssets.isEmpty) {
@@ -454,7 +477,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 
     final hasPermission = _galleryPermissionNotifier.hasPermission;
     if (hasPermission) {
-      await PhotoManager.clearFileCache();
+      await _fileMediaRepository.clearFileCache();
 
       if (state.allUniqueAssets.isEmpty) {
         log.info("No Asset On Device - Abort Backup Process");
@@ -465,7 +488,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
       Set<BackupCandidate> assetsWillBeBackup = Set.from(state.allUniqueAssets);
       // Remove item that has already been backed up
       for (final assetId in state.allAssetsInDatabase) {
-        assetsWillBeBackup.removeWhere((e) => e.asset.id == assetId);
+        assetsWillBeBackup.removeWhere((e) => e.asset.localId == assetId);
       }
 
       if (assetsWillBeBackup.isEmpty) {
@@ -531,7 +554,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
       state = state.copyWith(
         allUniqueAssets: state.allUniqueAssets
             .where(
-              (candidate) => candidate.asset.id != result.candidate.asset.id,
+              (candidate) =>
+                  candidate.asset.localId != result.candidate.asset.localId,
             )
             .toSet(),
       );
@@ -539,11 +563,11 @@ class BackupNotifier extends StateNotifier<BackUpState> {
       state = state.copyWith(
         selectedAlbumsBackupAssetsIds: {
           ...state.selectedAlbumsBackupAssetsIds,
-          result.candidate.asset.id,
+          result.candidate.asset.localId!,
         },
         allAssetsInDatabase: [
           ...state.allAssetsInDatabase,
-          result.candidate.asset.id,
+          result.candidate.asset.localId!,
         ],
       );
     }
@@ -552,7 +576,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
             state.selectedAlbumsBackupAssetsIds.length ==
         0) {
       final latestAssetBackup = state.allUniqueAssets
-          .map((candidate) => candidate.asset.modifiedDateTime)
+          .map((candidate) => candidate.asset.fileModifiedAt)
           .reduce(
             (v, e) => e.isAfter(v) ? e : v,
           );
@@ -741,6 +765,8 @@ final backupProvider =
     ref.watch(backgroundServiceProvider),
     ref.watch(galleryPermissionNotifier.notifier),
     ref.watch(dbProvider),
+    ref.watch(albumMediaRepositoryProvider),
+    ref.watch(fileMediaRepositoryProvider),
     ref,
   );
 });
diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart
index a76b56fea7..0cf159bfdd 100644
--- a/mobile/lib/providers/backup/manual_upload.provider.dart
+++ b/mobile/lib/providers/backup/manual_upload.provider.dart
@@ -8,6 +8,7 @@ import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
 import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
 import 'package:immich_mobile/services/background.service.dart';
 import 'package:immich_mobile/models/backup/backup_state.model.dart';
 import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
@@ -27,7 +28,7 @@ import 'package:immich_mobile/utils/backup_progress.dart';
 import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
 import 'package:permission_handler/permission_handler.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
 
 final manualUploadProvider =
     StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
@@ -193,17 +194,10 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
       _backupProvider.updateBackupProgress(BackUpProgressEnum.manualInProgress);
 
       if (ref.read(galleryPermissionNotifier.notifier).hasPermission) {
-        await PhotoManager.clearFileCache();
+        await ref.read(fileMediaRepositoryProvider).clearFileCache();
 
-        // We do not have 1:1 mapping of all AssetEntity fields to Asset. This results in cases
-        // where platform specific fields such as `subtype` used to detect platform specific assets such as
-        // LivePhoto in iOS is lost when we directly fetch the local asset from Asset using Asset.local
-        List<AssetEntity?> allAssetsFromDevice = await Future.wait(
-          allManualUploads
-              // Filter local only assets
-              .where((e) => e.isLocal && !e.isRemote)
-              .map((e) => e.local!.obtainForNewProperties()),
-        );
+        final allAssetsFromDevice =
+            allManualUploads.where((e) => e.isLocal && !e.isRemote).toList();
 
         if (allAssetsFromDevice.length != allManualUploads.length) {
           _log.warning(
@@ -221,11 +215,17 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
             await _backupService.buildUploadCandidates(
           selectedBackupAlbums,
           excludedBackupAlbums,
+          useTimeFilter: false,
         );
 
-        // Extrack candidate from allAssetsFromDevice.nonNulls
-        final uploadAssets = candidates
-            .where((e) => allAssetsFromDevice.nonNulls.contains(e.asset));
+        // Extrack candidate from allAssetsFromDevice
+        final uploadAssets = candidates.where(
+          (candidate) =>
+              allAssetsFromDevice.firstWhereOrNull(
+                (asset) => asset.localId == candidate.asset.localId,
+              ) !=
+              null,
+        );
 
         if (uploadAssets.isEmpty) {
           debugPrint("[_startUpload] No Assets to upload - Abort Process");
diff --git a/mobile/lib/providers/image/immich_local_image_provider.dart b/mobile/lib/providers/image/immich_local_image_provider.dart
index dc1b8a9845..c1bafa6c5a 100644
--- a/mobile/lib/providers/image/immich_local_image_provider.dart
+++ b/mobile/lib/providers/image/immich_local_image_provider.dart
@@ -9,7 +9,7 @@ import 'package:flutter/painting.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/services/app_settings.service.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show ThumbnailSize;
 
 /// The local image provider for an asset
 class ImmichLocalImageProvider extends ImageProvider<ImmichLocalImageProvider> {
diff --git a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart
index 28e78ae762..69cdb105c0 100644
--- a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart
+++ b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart
@@ -6,7 +6,7 @@ import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/painting.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show ThumbnailSize;
 
 /// The local image provider for an asset
 /// Only viable
diff --git a/mobile/lib/repositories/album_media.repository.dart b/mobile/lib/repositories/album_media.repository.dart
new file mode 100644
index 0000000000..c3795f75df
--- /dev/null
+++ b/mobile/lib/repositories/album_media.repository.dart
@@ -0,0 +1,93 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
+import 'package:immich_mobile/repositories/asset_media.repository.dart';
+import 'package:photo_manager/photo_manager.dart' hide AssetType;
+
+final albumMediaRepositoryProvider = Provider((ref) => AlbumMediaRepository());
+
+class AlbumMediaRepository implements IAlbumMediaRepository {
+  @override
+  Future<List<Album>> getAll() async {
+    final List<AssetPathEntity> assetPathEntities =
+        await PhotoManager.getAssetPathList(
+      hasAll: true,
+      filterOption: FilterOptionGroup(containsPathModified: true),
+    );
+    return assetPathEntities.map(_toAlbum).toList();
+  }
+
+  @override
+  Future<List<String>> getAssetIds(String albumId) async {
+    final album = await AssetPathEntity.fromId(albumId);
+    final List<AssetEntity> assets =
+        await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
+    return assets.map((e) => e.id).toList();
+  }
+
+  @override
+  Future<int> getAssetCount(String albumId) async {
+    final album = await AssetPathEntity.fromId(albumId);
+    return album.assetCountAsync;
+  }
+
+  @override
+  Future<List<Asset>> getAssets(
+    String albumId, {
+    int start = 0,
+    int end = 0x7fffffffffffffff,
+    DateTime? modifiedFrom,
+    DateTime? modifiedUntil,
+    bool orderByModificationDate = false,
+  }) async {
+    final onDevice = await AssetPathEntity.fromId(
+      albumId,
+      filterOption: FilterOptionGroup(
+        containsPathModified: true,
+        orders: orderByModificationDate
+            ? [const OrderOption(type: OrderOptionType.updateDate)]
+            : [],
+        imageOption: const FilterOption(needTitle: true),
+        videoOption: const FilterOption(needTitle: true),
+        updateTimeCond: modifiedFrom == null && modifiedUntil == null
+            ? null
+            : DateTimeCond(
+                min: modifiedFrom ?? DateTime.utc(-271820),
+                max: modifiedUntil ?? DateTime.utc(275760),
+              ),
+      ),
+    );
+
+    final List<AssetEntity> assets =
+        await onDevice.getAssetListRange(start: start, end: end);
+    return assets.map(AssetMediaRepository.toAsset).toList().cast();
+  }
+
+  @override
+  Future<Album> get(
+    String id, {
+    DateTime? modifiedFrom,
+    DateTime? modifiedUntil,
+  }) async {
+    final assetPathEntity = await AssetPathEntity.fromId(id);
+    return _toAlbum(assetPathEntity);
+  }
+
+  static Album _toAlbum(AssetPathEntity assetPathEntity) {
+    final Album album = Album(
+      name: assetPathEntity.name,
+      createdAt:
+          assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(),
+      modifiedAt:
+          assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(),
+      shared: false,
+      activityEnabled: false,
+    );
+    album.owner.value = Store.get(StoreKey.currentUser);
+    album.localId = assetPathEntity.id;
+    album.isAll = assetPathEntity.isAll;
+    return album;
+  }
+}
diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart
new file mode 100644
index 0000000000..20cf680339
--- /dev/null
+++ b/mobile/lib/repositories/asset_media.repository.dart
@@ -0,0 +1,46 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/entities/exif_info.entity.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/interfaces/asset_media.interface.dart';
+import 'package:photo_manager/photo_manager.dart' hide AssetType;
+
+final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository());
+
+class AssetMediaRepository implements IAssetMediaRepository {
+  @override
+  Future<List<String>> deleteAll(List<String> ids) =>
+      PhotoManager.editor.deleteWithIds(ids);
+
+  @override
+  Future<Asset?> get(String id) async {
+    final entity = await AssetEntity.fromId(id);
+    return toAsset(entity);
+  }
+
+  static Asset? toAsset(AssetEntity? local) {
+    if (local == null) return null;
+    final Asset asset = Asset(
+      checksum: "",
+      localId: local.id,
+      ownerId: Store.get(StoreKey.currentUser).isarId,
+      fileCreatedAt: local.createDateTime,
+      fileModifiedAt: local.modifiedDateTime,
+      updatedAt: local.modifiedDateTime,
+      durationInSeconds: local.duration,
+      type: AssetType.values[local.typeInt],
+      fileName: local.title!,
+      width: local.width,
+      height: local.height,
+      isFavorite: local.isFavorite,
+    );
+    if (asset.fileCreatedAt.year == 1970) {
+      asset.fileCreatedAt = asset.fileModifiedAt;
+    }
+    if (local.latitude != null) {
+      asset.exifInfo = ExifInfo(lat: local.latitude, long: local.longitude);
+    }
+    asset.local = local;
+    return asset;
+  }
+}
diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart
new file mode 100644
index 0000000000..e115868ba0
--- /dev/null
+++ b/mobile/lib/repositories/file_media.repository.dart
@@ -0,0 +1,62 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
+import 'package:immich_mobile/repositories/asset_media.repository.dart';
+import 'package:photo_manager/photo_manager.dart' hide AssetType;
+
+final fileMediaRepositoryProvider = Provider((ref) => FileMediaRepository());
+
+class FileMediaRepository implements IFileMediaRepository {
+  @override
+  Future<Asset?> saveImage(
+    Uint8List data, {
+    required String title,
+    String? relativePath,
+  }) async {
+    final entity = await PhotoManager.editor
+        .saveImage(data, title: title, relativePath: relativePath);
+    return AssetMediaRepository.toAsset(entity);
+  }
+
+  @override
+  Future<Asset?> saveLivePhoto({
+    required File image,
+    required File video,
+    required String title,
+  }) async {
+    final entity = await PhotoManager.editor.darwin.saveLivePhoto(
+      imageFile: image,
+      videoFile: video,
+      title: title,
+    );
+    return AssetMediaRepository.toAsset(entity);
+  }
+
+  @override
+  Future<Asset?> saveVideo(
+    File file, {
+    required String title,
+    String? relativePath,
+  }) async {
+    final entity = await PhotoManager.editor.saveVideo(
+      file,
+      title: title,
+      relativePath: relativePath,
+    );
+    return AssetMediaRepository.toAsset(entity);
+  }
+
+  @override
+  Future<void> clearFileCache() => PhotoManager.clearFileCache();
+
+  @override
+  Future<void> enableBackgroundAccess() =>
+      PhotoManager.setIgnorePermissionCheck(true);
+
+  @override
+  Future<void> requestExtendedPermissions() =>
+      PhotoManager.requestPermissionExtend();
+}
diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart
index 211c847726..6869e7b704 100644
--- a/mobile/lib/routing/router.dart
+++ b/mobile/lib/routing/router.dart
@@ -63,7 +63,6 @@ import 'package:immich_mobile/services/api.service.dart';
 import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
 import 'package:isar/isar.dart';
 import 'package:maplibre_gl/maplibre_gl.dart';
-import 'package:photo_manager/photo_manager.dart' hide LatLng;
 
 part 'router.gr.dart';
 
diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart
index 90fc4cb0fe..df4c29fba1 100644
--- a/mobile/lib/routing/router.gr.dart
+++ b/mobile/lib/routing/router.gr.dart
@@ -185,7 +185,7 @@ class AlbumOptionsRouteArgs {
 class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> {
   AlbumPreviewRoute({
     Key? key,
-    required AssetPathEntity album,
+    required Album album,
     List<PageRouteInfo>? children,
   }) : super(
           AlbumPreviewRoute.name,
@@ -218,7 +218,7 @@ class AlbumPreviewRouteArgs {
 
   final Key? key;
 
-  final AssetPathEntity album;
+  final Album album;
 
   @override
   String toString() {
diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart
index 92302a0d88..104c3827cb 100644
--- a/mobile/lib/services/album.service.dart
+++ b/mobile/lib/services/album.service.dart
@@ -6,6 +6,7 @@ import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/interfaces/album.interface.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
 import 'package:immich_mobile/interfaces/asset.interface.dart';
 import 'package:immich_mobile/interfaces/backup.interface.dart';
 import 'package:immich_mobile/interfaces/user.interface.dart';
@@ -19,13 +20,13 @@ import 'package:immich_mobile/providers/api.provider.dart';
 import 'package:immich_mobile/repositories/album.repository.dart';
 import 'package:immich_mobile/repositories/asset.repository.dart';
 import 'package:immich_mobile/repositories/backup.repository.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
 import 'package:immich_mobile/repositories/user.repository.dart';
 import 'package:immich_mobile/services/api.service.dart';
 import 'package:immich_mobile/services/sync.service.dart';
 import 'package:immich_mobile/services/user.service.dart';
 import 'package:logging/logging.dart';
 import 'package:openapi/api.dart';
-import 'package:photo_manager/photo_manager.dart';
 
 final albumServiceProvider = Provider(
   (ref) => AlbumService(
@@ -36,6 +37,7 @@ final albumServiceProvider = Provider(
     ref.watch(assetRepositoryProvider),
     ref.watch(userRepositoryProvider),
     ref.watch(backupRepositoryProvider),
+    ref.watch(albumMediaRepositoryProvider),
   ),
 );
 
@@ -47,6 +49,7 @@ class AlbumService {
   final IAssetRepository _assetRepository;
   final IUserRepository _userRepository;
   final IBackupRepository _backupAlbumRepository;
+  final IAlbumMediaRepository _albumMediaRepository;
   final Logger _log = Logger('AlbumService');
   Completer<bool> _localCompleter = Completer()..complete(false);
   Completer<bool> _remoteCompleter = Completer()..complete(false);
@@ -59,6 +62,7 @@ class AlbumService {
     this._assetRepository,
     this._userRepository,
     this._backupAlbumRepository,
+    this._albumMediaRepository,
   );
 
   /// Checks all selected device albums for changes of albums and their assets
@@ -84,11 +88,7 @@ class AlbumService {
         }
         return false;
       }
-      final List<AssetPathEntity> onDevice =
-          await PhotoManager.getAssetPathList(
-        hasAll: true,
-        filterOption: FilterOptionGroup(containsPathModified: true),
-      );
+      final List<Album> onDevice = await _albumMediaRepository.getAll();
       _log.info("Found ${onDevice.length} device albums");
       Set<String>? excludedAssets;
       if (excludedIds.isNotEmpty) {
@@ -104,13 +104,15 @@ class AlbumService {
           _log.info("Found ${excludedAssets.length} assets to exclude");
         }
         // remove all excluded albums
-        onDevice.removeWhere((e) => excludedIds.contains(e.id));
+        onDevice.removeWhere((e) => excludedIds.contains(e.localId));
         _log.info(
           "Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums",
         );
       }
       final hasAll = selectedIds
-          .map((id) => onDevice.firstWhereOrNull((a) => a.id == id))
+          .map(
+            (id) => onDevice.firstWhereOrNull((album) => album.localId == id),
+          )
           .whereNotNull()
           .any((a) => a.isAll);
       if (hasAll) {
@@ -122,7 +124,7 @@ class AlbumService {
         }
       } else {
         // keep only the explicitly selected albums
-        onDevice.removeWhere((e) => !selectedIds.contains(e.id));
+        onDevice.removeWhere((e) => !selectedIds.contains(e.localId));
         _log.info("'Recents' is not selected, keeping only selected albums");
       }
       changes =
@@ -136,15 +138,15 @@ class AlbumService {
   }
 
   Future<Set<String>> _loadExcludedAssetIds(
-    List<AssetPathEntity> albums,
+    List<Album> albums,
     List<String> excludedAlbumIds,
   ) async {
     final Set<String> result = HashSet<String>();
-    for (AssetPathEntity a in albums) {
-      if (excludedAlbumIds.contains(a.id)) {
-        final List<AssetEntity> assets =
-            await a.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
-        result.addAll(assets.map((e) => e.id));
+    for (Album album in albums) {
+      if (excludedAlbumIds.contains(album.localId)) {
+        final assetIds =
+            await _albumMediaRepository.getAssetIds(album.localId!);
+        result.addAll(assetIds);
       }
     }
     return result;
diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart
index c4f258e259..90c46ae90a 100644
--- a/mobile/lib/services/asset.service.dart
+++ b/mobile/lib/services/asset.service.dart
@@ -321,7 +321,7 @@ class AssetService {
 
       for (BackupCandidate candidate in candidates) {
         final asset = remoteAssets.firstWhereOrNull(
-          (a) => a.localId == candidate.asset.id,
+          (a) => a.localId == candidate.asset.localId,
         );
 
         if (asset != null) {
diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart
index 0d4d547434..f10abd7297 100644
--- a/mobile/lib/services/background.service.dart
+++ b/mobile/lib/services/background.service.dart
@@ -15,6 +15,8 @@ import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
 import 'package:immich_mobile/repositories/album.repository.dart';
 import 'package:immich_mobile/repositories/asset.repository.dart';
 import 'package:immich_mobile/repositories/backup.repository.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
 import 'package:immich_mobile/repositories/user.repository.dart';
 import 'package:immich_mobile/services/album.service.dart';
 import 'package:immich_mobile/services/hash.service.dart';
@@ -34,7 +36,7 @@ import 'package:immich_mobile/utils/diff.dart';
 import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
 import 'package:isar/isar.dart';
 import 'package:path_provider_ios/path_provider_ios.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
 
 final backgroundServiceProvider = Provider(
   (ref) => BackgroundService(),
@@ -363,8 +365,10 @@ class BackgroundService {
     AssetRepository assetRepository = AssetRepository(db);
     UserRepository userRepository = UserRepository(db);
     BackupRepository backupAlbumRepository = BackupRepository(db);
-    HashService hashService = HashService(db, this);
-    SyncService syncSerive = SyncService(db, hashService);
+    AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
+    FileMediaRepository fileMediaRepository = FileMediaRepository();
+    HashService hashService = HashService(db, this, albumMediaRepository);
+    SyncService syncSerive = SyncService(db, hashService, albumMediaRepository);
     UserService userService =
         UserService(apiService, db, syncSerive, partnerService);
     AlbumService albumService = AlbumService(
@@ -375,9 +379,16 @@ class BackgroundService {
       assetRepository,
       userRepository,
       backupAlbumRepository,
+      albumMediaRepository,
+    );
+    BackupService backupService = BackupService(
+      apiService,
+      db,
+      settingService,
+      albumService,
+      albumMediaRepository,
+      fileMediaRepository,
     );
-    BackupService backupService =
-        BackupService(apiService, db, settingService, albumService);
 
     final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
     final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync();
@@ -385,7 +396,7 @@ class BackgroundService {
       return true;
     }
 
-    await PhotoManager.setIgnorePermissionCheck(true);
+    await fileMediaRepository.enableBackgroundAccess();
 
     do {
       final bool backupOk = await _runBackup(
diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart
index 858499443e..19d731d773 100644
--- a/mobile/lib/services/backup.service.dart
+++ b/mobile/lib/services/backup.service.dart
@@ -6,9 +6,13 @@ import 'package:cancellation_token_http/http.dart' as http;
 import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/entities/backup_album.entity.dart';
 import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
 import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
 import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
 import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
@@ -16,6 +20,8 @@ import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
 import 'package:immich_mobile/providers/api.provider.dart';
 import 'package:immich_mobile/providers/app_settings.provider.dart';
 import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
 import 'package:immich_mobile/services/album.service.dart';
 import 'package:immich_mobile/services/api.service.dart';
 import 'package:immich_mobile/services/app_settings.service.dart';
@@ -24,7 +30,7 @@ import 'package:logging/logging.dart';
 import 'package:openapi/api.dart';
 import 'package:path/path.dart' as p;
 import 'package:permission_handler/permission_handler.dart' as pm;
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
 
 final backupServiceProvider = Provider(
   (ref) => BackupService(
@@ -32,6 +38,8 @@ final backupServiceProvider = Provider(
     ref.watch(dbProvider),
     ref.watch(appSettingsServiceProvider),
     ref.watch(albumServiceProvider),
+    ref.watch(albumMediaRepositoryProvider),
+    ref.watch(fileMediaRepositoryProvider),
   ),
 );
 
@@ -42,12 +50,16 @@ class BackupService {
   final Logger _log = Logger("BackupService");
   final AppSettingsService _appSetting;
   final AlbumService _albumService;
+  final IAlbumMediaRepository _albumMediaRepository;
+  final IFileMediaRepository _fileMediaRepository;
 
   BackupService(
     this._apiService,
     this._db,
     this._appSetting,
     this._albumService,
+    this._albumMediaRepository,
+    this._fileMediaRepository,
   );
 
   Future<List<String>?> getDeviceBackupAsset() async {
@@ -86,44 +98,17 @@ class BackupService {
     List<BackupAlbum> excludedBackupAlbums, {
     bool useTimeFilter = true,
   }) async {
-    final filter = FilterOptionGroup(
-      containsPathModified: true,
-      orders: [const OrderOption(type: OrderOptionType.updateDate)],
-      // title is needed to create Assets
-      imageOption: const FilterOption(needTitle: true),
-      videoOption: const FilterOption(needTitle: true),
-    );
     final now = DateTime.now();
 
-    final List<AssetPathEntity?> selectedAlbums =
-        await _loadAlbumsWithTimeFilter(
-      selectedBackupAlbums,
-      filter,
-      now,
-      useTimeFilter: useTimeFilter,
-    );
-
-    if (selectedAlbums.every((e) => e == null)) {
-      return {};
-    }
-
-    final List<AssetPathEntity?> excludedAlbums =
-        await _loadAlbumsWithTimeFilter(
-      excludedBackupAlbums,
-      filter,
-      now,
-      useTimeFilter: useTimeFilter,
-    );
-
     final Set<BackupCandidate> toAdd = await _fetchAssetsAndUpdateLastBackup(
-      selectedAlbums,
       selectedBackupAlbums,
       now,
       useTimeFilter: useTimeFilter,
     );
 
+    if (toAdd.isEmpty) return {};
+
     final Set<BackupCandidate> toRemove = await _fetchAssetsAndUpdateLastBackup(
-      excludedAlbums,
       excludedBackupAlbums,
       now,
       useTimeFilter: useTimeFilter,
@@ -132,92 +117,62 @@ class BackupService {
     return toAdd.difference(toRemove);
   }
 
-  Future<List<AssetPathEntity?>> _loadAlbumsWithTimeFilter(
-    List<BackupAlbum> albums,
-    FilterOptionGroup filter,
-    DateTime now, {
-    bool useTimeFilter = true,
-  }) async {
-    List<AssetPathEntity?> result = [];
-    for (BackupAlbum backupAlbum in albums) {
-      try {
-        final optionGroup = useTimeFilter
-            ? filter.copyWith(
-                updateTimeCond: DateTimeCond(
-                  // subtract 2 seconds to prevent missing assets due to rounding issues
-                  min: backupAlbum.lastBackup
-                      .subtract(const Duration(seconds: 2)),
-                  max: now,
-                ),
-              )
-            : filter;
-
-        final AssetPathEntity album =
-            await AssetPathEntity.obtainPathFromProperties(
-          id: backupAlbum.id,
-          optionGroup: optionGroup,
-          maxDateTimeToNow: false,
-        );
-
-        result.add(album);
-      } on StateError {
-        // either there are no assets matching the filter criteria OR the album no longer exists
-      }
-    }
-
-    return result;
-  }
-
   Future<Set<BackupCandidate>> _fetchAssetsAndUpdateLastBackup(
-    List<AssetPathEntity?> localAlbums,
     List<BackupAlbum> backupAlbums,
     DateTime now, {
     bool useTimeFilter = true,
   }) async {
-    Set<BackupCandidate> candidate = {};
+    Set<BackupCandidate> candidates = {};
 
-    for (int i = 0; i < localAlbums.length; i++) {
-      final localAlbum = localAlbums[i];
-      if (localAlbum == null) {
+    for (final BackupAlbum backupAlbum in backupAlbums) {
+      final Album localAlbum;
+      try {
+        localAlbum = await _albumMediaRepository.get(backupAlbum.id);
+      } on StateError {
+        // the album no longer exists
         continue;
       }
 
       if (useTimeFilter &&
-          localAlbum.lastModified?.isBefore(backupAlbums[i].lastBackup) ==
-              true) {
+          localAlbum.modifiedAt.isBefore(backupAlbum.lastBackup)) {
+        continue;
+      }
+      final List<Asset> assets;
+      try {
+        assets = await _albumMediaRepository.getAssets(
+          backupAlbum.id,
+          modifiedFrom: useTimeFilter
+              ?
+              // subtract 2 seconds to prevent missing assets due to rounding issues
+              backupAlbum.lastBackup.subtract(const Duration(seconds: 2))
+              : null,
+          modifiedUntil: useTimeFilter ? now : null,
+        );
+      } on StateError {
+        // either there are no assets matching the filter criteria OR the album no longer exists
         continue;
       }
-
-      final assets = await localAlbum.getAssetListRange(
-        start: 0,
-        end: await localAlbum.assetCountAsync,
-      );
 
       // Add album's name to the asset info
       for (final asset in assets) {
         List<String> albumNames = [localAlbum.name];
 
-        final existingAsset = candidate.firstWhereOrNull(
-          (a) => a.asset.id == asset.id,
+        final existingAsset = candidates.firstWhereOrNull(
+          (candidate) => candidate.asset.localId == asset.localId,
         );
 
         if (existingAsset != null) {
           albumNames.addAll(existingAsset.albumNames);
-          candidate.remove(existingAsset);
+          candidates.remove(existingAsset);
         }
 
-        candidate.add(
-          BackupCandidate(
-            asset: asset,
-            albumNames: albumNames,
-          ),
-        );
+        candidates.add(BackupCandidate(asset: asset, albumNames: albumNames));
       }
 
-      backupAlbums[i].lastBackup = now;
+      backupAlbum.lastBackup = now;
     }
 
-    return candidate;
+    return candidates;
   }
 
   /// Returns a new list of assets not yet uploaded
@@ -230,7 +185,7 @@ class BackupService {
 
     final Set<String> duplicatedAssetIds = await getDuplicatedAssetIds();
     candidates.removeWhere(
-      (candidate) => duplicatedAssetIds.contains(candidate.asset.id),
+      (candidate) => duplicatedAssetIds.contains(candidate.asset.localId),
     );
 
     if (candidates.isEmpty) {
@@ -243,7 +198,7 @@ class BackupService {
       final CheckExistingAssetsResponseDto? duplicates =
           await _apiService.assetsApi.checkExistingAssets(
         CheckExistingAssetsDto(
-          deviceAssetIds: candidates.map((c) => c.asset.id).toList(),
+          deviceAssetIds: candidates.map((c) => c.asset.localId!).toList(),
           deviceId: deviceId,
         ),
       );
@@ -259,7 +214,7 @@ class BackupService {
     }
 
     if (existing.isNotEmpty) {
-      candidates.removeWhere((c) => existing.contains(c.asset.id));
+      candidates.removeWhere((c) => existing.contains(c.asset.localId));
     }
 
     return candidates;
@@ -278,7 +233,7 @@ class BackupService {
 
     // DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS
     if (Platform.isIOS) {
-      await PhotoManager.requestPermissionExtend();
+      await _fileMediaRepository.requestExtendedPermissions();
     }
 
     return true;
@@ -289,9 +244,9 @@ class BackupService {
   List<BackupCandidate> _sortPhotosFirst(List<BackupCandidate> candidates) {
     return candidates.sorted(
       (a, b) {
-        final cmp = a.asset.typeInt - b.asset.typeInt;
+        final cmp = a.asset.type.index - b.asset.type.index;
         if (cmp != 0) return cmp;
-        return a.asset.createDateTime.compareTo(b.asset.createDateTime);
+        return a.asset.fileCreatedAt.compareTo(b.asset.fileCreatedAt);
       },
     );
   }
@@ -325,13 +280,13 @@ class BackupService {
     }
 
     for (final candidate in candidates) {
-      final AssetEntity entity = candidate.asset;
+      final Asset asset = candidate.asset;
       File? file;
       File? livePhotoFile;
 
       try {
         final isAvailableLocally =
-            await entity.isLocallyAvailable(isOrigin: true);
+            await asset.local!.isLocallyAvailable(isOrigin: true);
 
         // Handle getting files from iCloud
         if (!isAvailableLocally && Platform.isIOS) {
@@ -342,39 +297,41 @@ class BackupService {
 
           onCurrentAsset(
             CurrentUploadAsset(
-              id: entity.id,
-              fileCreatedAt: entity.createDateTime.year == 1970
-                  ? entity.modifiedDateTime
-                  : entity.createDateTime,
-              fileName: await entity.titleAsync,
-              fileType: _getAssetType(entity.type),
+              id: asset.localId!,
+              fileCreatedAt: asset.fileCreatedAt.year == 1970
+                  ? asset.fileModifiedAt
+                  : asset.fileCreatedAt,
+              fileName: asset.fileName,
+              fileType: _getAssetType(asset.type),
               iCloudAsset: true,
             ),
           );
 
-          file = await entity.loadFile(progressHandler: pmProgressHandler);
-          if (entity.isLivePhoto) {
-            livePhotoFile = await entity.loadFile(
+          file =
+              await asset.local!.loadFile(progressHandler: pmProgressHandler);
+          if (asset.local!.isLivePhoto) {
+            livePhotoFile = await asset.local!.loadFile(
               withSubtype: true,
               progressHandler: pmProgressHandler,
             );
           }
         } else {
-          if (entity.type == AssetType.video) {
-            file = await entity.originFile;
+          if (asset.type == AssetType.video) {
+            file = await asset.local!.originFile;
           } else {
-            file = await entity.originFile.timeout(const Duration(seconds: 5));
-            if (entity.isLivePhoto) {
-              livePhotoFile = await entity.originFileWithSubtype
+            file = await asset.local!.originFile
+                .timeout(const Duration(seconds: 5));
+            if (asset.local!.isLivePhoto) {
+              livePhotoFile = await asset.local!.originFileWithSubtype
                   .timeout(const Duration(seconds: 5));
             }
           }
         }
 
         if (file != null) {
-          String originalFileName = await entity.titleAsync;
+          String originalFileName = asset.fileName;
 
-          if (entity.isLivePhoto) {
+          if (asset.local!.isLivePhoto) {
             if (livePhotoFile == null) {
               _log.warning(
                 "Failed to obtain motion part of the livePhoto - $originalFileName",
@@ -398,31 +355,31 @@ class BackupService {
 
           baseRequest.headers.addAll(ApiService.getRequestHeaders());
           baseRequest.headers["Transfer-Encoding"] = "chunked";
-          baseRequest.fields['deviceAssetId'] = entity.id;
+          baseRequest.fields['deviceAssetId'] = asset.localId!;
           baseRequest.fields['deviceId'] = deviceId;
           baseRequest.fields['fileCreatedAt'] =
-              entity.createDateTime.toUtc().toIso8601String();
+              asset.fileCreatedAt.toUtc().toIso8601String();
           baseRequest.fields['fileModifiedAt'] =
-              entity.modifiedDateTime.toUtc().toIso8601String();
-          baseRequest.fields['isFavorite'] = entity.isFavorite.toString();
-          baseRequest.fields['duration'] = entity.videoDuration.toString();
+              asset.fileModifiedAt.toUtc().toIso8601String();
+          baseRequest.fields['isFavorite'] = asset.isFavorite.toString();
+          baseRequest.fields['duration'] = asset.duration.toString();
           baseRequest.files.add(assetRawUploadData);
 
           onCurrentAsset(
             CurrentUploadAsset(
-              id: entity.id,
-              fileCreatedAt: entity.createDateTime.year == 1970
-                  ? entity.modifiedDateTime
-                  : entity.createDateTime,
+              id: asset.localId!,
+              fileCreatedAt: asset.fileCreatedAt.year == 1970
+                  ? asset.fileModifiedAt
+                  : asset.fileCreatedAt,
               fileName: originalFileName,
-              fileType: _getAssetType(entity.type),
+              fileType: _getAssetType(asset.type),
               fileSize: file.lengthSync(),
               iCloudAsset: false,
             ),
           );
 
           String? livePhotoVideoId;
-          if (entity.isLivePhoto && livePhotoFile != null) {
+          if (asset.local!.isLivePhoto && livePhotoFile != null) {
             livePhotoVideoId = await uploadLivePhotoVideo(
               originalFileName,
               livePhotoFile,
@@ -448,16 +405,16 @@ class BackupService {
             final errorMessage = error['message'] ?? error['error'];
 
             debugPrint(
-              "Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}",
+              "Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}",
             );
 
             onError(
               ErrorUploadAsset(
-                asset: entity,
-                id: entity.id,
-                fileCreatedAt: entity.createDateTime,
+                asset: asset,
+                id: asset.localId!,
+                fileCreatedAt: asset.fileCreatedAt,
                 fileName: originalFileName,
-                fileType: _getAssetType(entity.type),
+                fileType: _getAssetType(candidate.asset.type),
                 errorMessage: errorMessage,
               ),
             );
@@ -473,7 +430,7 @@ class BackupService {
           bool isDuplicate = false;
           if (response.statusCode == 200) {
             isDuplicate = true;
-            duplicatedAssetIds.add(entity.id);
+            duplicatedAssetIds.add(asset.localId!);
           }
 
           onSuccess(
diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart
index c7cd134cb1..66a61d2914 100644
--- a/mobile/lib/services/backup_verification.service.dart
+++ b/mobile/lib/services/backup_verification.service.dart
@@ -8,17 +8,19 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/entities/exif_info.entity.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
 import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
 import 'package:immich_mobile/services/api.service.dart';
 import 'package:immich_mobile/utils/diff.dart';
 import 'package:isar/isar.dart';
-import 'package:photo_manager/photo_manager.dart' show PhotoManager;
 
 /// Finds duplicates originating from missing EXIF information
 class BackupVerificationService {
   final Isar _db;
+  final IFileMediaRepository _fileMediaRepository;
 
-  BackupVerificationService(this._db);
+  BackupVerificationService(this._db, this._fileMediaRepository);
 
   /// Returns at most [limit] assets that were backed up without exif
   Future<List<Asset>> findWronglyBackedUpAssets({int limit = 100}) async {
@@ -71,6 +73,7 @@ class BackupVerificationService {
           auth: Store.get(StoreKey.accessToken),
           endpoint: Store.get(StoreKey.serverEndpoint),
           rootIsolateToken: isolateToken,
+          fileMediaRepository: _fileMediaRepository,
         ),
       );
       final upper = compute(
@@ -81,6 +84,7 @@ class BackupVerificationService {
           auth: Store.get(StoreKey.accessToken),
           endpoint: Store.get(StoreKey.serverEndpoint),
           rootIsolateToken: isolateToken,
+          fileMediaRepository: _fileMediaRepository,
         ),
       );
       toDelete = await lower + await upper;
@@ -93,6 +97,7 @@ class BackupVerificationService {
           auth: Store.get(StoreKey.accessToken),
           endpoint: Store.get(StoreKey.serverEndpoint),
           rootIsolateToken: isolateToken,
+          fileMediaRepository: _fileMediaRepository,
         ),
       );
     }
@@ -106,12 +111,13 @@ class BackupVerificationService {
       String auth,
       String endpoint,
       RootIsolateToken rootIsolateToken,
+      IFileMediaRepository fileMediaRepository,
     }) tuple,
   ) async {
     assert(tuple.deleteCandidates.length == tuple.originals.length);
     final List<Asset> result = [];
     BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken);
-    await PhotoManager.setIgnorePermissionCheck(true);
+    await tuple.fileMediaRepository.enableBackgroundAccess();
     final ApiService apiService = ApiService();
     apiService.setEndpoint(tuple.endpoint);
     apiService.setAccessToken(tuple.auth);
@@ -228,5 +234,6 @@ class BackupVerificationService {
 final backupVerificationServiceProvider = Provider(
   (ref) => BackupVerificationService(
     ref.watch(dbProvider),
+    ref.watch(fileMediaRepositoryProvider),
   ),
 );
diff --git a/mobile/lib/services/hash.service.dart b/mobile/lib/services/hash.service.dart
index ffc81a3445..2ec545453f 100644
--- a/mobile/lib/services/hash.service.dart
+++ b/mobile/lib/services/hash.service.dart
@@ -2,6 +2,9 @@ import 'dart:io';
 
 import 'package:flutter/foundation.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
 import 'package:immich_mobile/services/background.service.dart';
 import 'package:immich_mobile/entities/android_device_asset.entity.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
@@ -11,38 +14,46 @@ import 'package:immich_mobile/providers/db.provider.dart';
 import 'package:immich_mobile/extensions/string_extensions.dart';
 import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
-import 'package:photo_manager/photo_manager.dart';
 
 class HashService {
-  HashService(this._db, this._backgroundService);
+  HashService(this._db, this._backgroundService, this._albumMediaRepository);
   final Isar _db;
   final BackgroundService _backgroundService;
+  final IAlbumMediaRepository _albumMediaRepository;
   final _log = Logger('HashService');
 
   /// Returns all assets that were successfully hashed
   Future<List<Asset>> getHashedAssets(
-    AssetPathEntity album, {
+    Album album, {
     int start = 0,
     int end = 0x7fffffffffffffff,
+    DateTime? modifiedFrom,
+    DateTime? modifiedUntil,
     Set<String>? excludedAssets,
   }) async {
-    final entities = await album.getAssetListRange(start: start, end: end);
+    final entities = await _albumMediaRepository.getAssets(
+      album.localId!,
+      start: start,
+      end: end,
+      modifiedFrom: modifiedFrom,
+      modifiedUntil: modifiedUntil,
+    );
     final filtered = excludedAssets == null
         ? entities
-        : entities.where((e) => !excludedAssets.contains(e.id)).toList();
+        : entities.where((e) => !excludedAssets.contains(e.localId!)).toList();
     return _hashAssets(filtered);
   }
 
-  /// Converts a list of [AssetEntity]s to [Asset]s including only those
+  /// Processes a list of local [Asset]s, storing their hash and returning only those
   /// that were successfully hashed. Hashes are looked up in a DB table
   /// [AndroidDeviceAsset] / [IOSDeviceAsset] by local id. Only missing
   /// entries are newly hashed and added to the DB table.
-  Future<List<Asset>> _hashAssets(List<AssetEntity> assetEntities) async {
+  Future<List<Asset>> _hashAssets(List<Asset> assets) async {
     const int batchFileCount = 128;
     const int batchDataSize = 1024 * 1024 * 1024; // 1GB
 
-    final ids = assetEntities
-        .map(Platform.isAndroid ? (a) => a.id.toInt() : (a) => a.id)
+    final ids = assets
+        .map(Platform.isAndroid ? (a) => a.localId!.toInt() : (a) => a.localId!)
         .toList();
     final List<DeviceAsset?> hashes = await _lookupHashes(ids);
     final List<DeviceAsset> toAdd = [];
@@ -50,22 +61,16 @@ class HashService {
 
     int bytes = 0;
 
-    for (int i = 0; i < assetEntities.length; i++) {
+    for (int i = 0; i < assets.length; i++) {
       if (hashes[i] != null) {
         continue;
       }
-      final file = await assetEntities[i].originFile;
+      final file = await assets[i].local!.originFile;
       if (file == null) {
-        final fileName = await assetEntities[i].titleAsync.catchError((error) {
-          _log.warning(
-            "Failed to get title for asset ${assetEntities[i].id}",
-          );
-
-          return "";
-        });
+        final fileName = assets[i].fileName;
 
         _log.warning(
-          "Failed to get file for asset ${assetEntities[i].id}, name: $fileName, created on: ${assetEntities[i].createDateTime}, skipping",
+          "Failed to get file for asset ${assets[i].localId}, name: $fileName, created on: ${assets[i].fileCreatedAt}, skipping",
         );
         continue;
       }
@@ -86,7 +91,7 @@ class HashService {
     if (toHash.isNotEmpty) {
       await _processBatch(toHash, toAdd);
     }
-    return _mapAllHashedAssets(assetEntities, hashes);
+    return _getHashedAssets(assets, hashes);
   }
 
   /// Lookup hashes of assets by their local ID
@@ -133,15 +138,16 @@ class HashService {
     return hashes;
   }
 
-  /// Converts [AssetEntity]s that were successfully hashed to [Asset]s
-  List<Asset> _mapAllHashedAssets(
-    List<AssetEntity> assets,
+  /// Returns all successfully hashed [Asset]s with their hash value set
+  List<Asset> _getHashedAssets(
+    List<Asset> assets,
     List<DeviceAsset?> hashes,
   ) {
     final List<Asset> result = [];
     for (int i = 0; i < assets.length; i++) {
       if (hashes[i] != null && hashes[i]!.hash.isNotEmpty) {
-        result.add(Asset.local(assets[i], hashes[i]!.hash));
+        assets[i].byteHash = hashes[i]!.hash;
+        result.add(assets[i]);
       }
     }
     return result;
@@ -152,5 +158,6 @@ final hashServiceProvider = Provider(
   (ref) => HashService(
     ref.watch(dbProvider),
     ref.watch(backgroundServiceProvider),
+    ref.watch(albumMediaRepositoryProvider),
   ),
 );
diff --git a/mobile/lib/services/image_viewer.service.dart b/mobile/lib/services/image_viewer.service.dart
index 9bcaba1d26..c94244175b 100644
--- a/mobile/lib/services/image_viewer.service.dart
+++ b/mobile/lib/services/image_viewer.service.dart
@@ -3,21 +3,27 @@ import 'dart:io';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/response_extensions.dart';
 import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
 import 'package:immich_mobile/providers/api.provider.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
 import 'package:immich_mobile/services/api.service.dart';
 import 'package:logging/logging.dart';
 
-import 'package:photo_manager/photo_manager.dart';
 import 'package:path_provider/path_provider.dart';
 
-final imageViewerServiceProvider =
-    Provider((ref) => ImageViewerService(ref.watch(apiServiceProvider)));
+final imageViewerServiceProvider = Provider(
+  (ref) => ImageViewerService(
+    ref.watch(apiServiceProvider),
+    ref.watch(fileMediaRepositoryProvider),
+  ),
+);
 
 class ImageViewerService {
   final ApiService _apiService;
+  final IFileMediaRepository _fileMediaRepository;
   final Logger _log = Logger("ImageViewerService");
 
-  ImageViewerService(this._apiService);
+  ImageViewerService(this._apiService, this._fileMediaRepository);
 
   Future<bool> downloadAsset(Asset asset) async {
     File? imageFile;
@@ -46,7 +52,7 @@ class ImageViewerService {
           return false;
         }
 
-        AssetEntity? entity;
+        Asset? resultAsset;
 
         final tempDir = await getTemporaryDirectory();
         videoFile = await File('${tempDir.path}/livephoto.mov').create();
@@ -54,24 +60,21 @@ class ImageViewerService {
         videoFile.writeAsBytesSync(motionResponse.bodyBytes);
         imageFile.writeAsBytesSync(imageResponse.bodyBytes);
 
-        entity = await PhotoManager.editor.darwin.saveLivePhoto(
-          imageFile: imageFile,
-          videoFile: videoFile,
+        resultAsset = await _fileMediaRepository.saveLivePhoto(
+          image: imageFile,
+          video: videoFile,
           title: asset.fileName,
         );
 
-        if (entity == null) {
+        if (resultAsset == null) {
           _log.warning(
             "Asset cannot be saved as a live photo. This is most likely a motion photo. Saving only the image file",
           );
-
-          entity = await PhotoManager.editor.saveImage(
-            imageResponse.bodyBytes,
-            title: asset.fileName,
-          );
+          resultAsset = await _fileMediaRepository
+              .saveImage(imageResponse.bodyBytes, title: asset.fileName);
         }
 
-        return entity != null;
+        return resultAsset != null;
       } else {
         var res = await _apiService.assetsApi
             .downloadAssetWithHttpInfo(asset.remoteId!);
@@ -81,11 +84,11 @@ class ImageViewerService {
           return false;
         }
 
-        final AssetEntity? entity;
+        final Asset? resultAsset;
         final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
 
         if (asset.isImage) {
-          entity = await PhotoManager.editor.saveImage(
+          resultAsset = await _fileMediaRepository.saveImage(
             res.bodyBytes,
             title: asset.fileName,
             relativePath: relativePath,
@@ -94,13 +97,13 @@ class ImageViewerService {
           final tempDir = await getTemporaryDirectory();
           videoFile = await File('${tempDir.path}/${asset.fileName}').create();
           videoFile.writeAsBytesSync(res.bodyBytes);
-          entity = await PhotoManager.editor.saveVideo(
+          resultAsset = await _fileMediaRepository.saveVideo(
             videoFile,
             title: asset.fileName,
             relativePath: relativePath,
           );
         }
-        return entity != null;
+        return resultAsset != null;
       }
     } catch (error, stack) {
       _log.severe("Error saving downloaded asset", error, stack);
diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart
index 8ec56e925f..84764b7641 100644
--- a/mobile/lib/services/sync.service.dart
+++ b/mobile/lib/services/sync.service.dart
@@ -8,7 +8,9 @@ import 'package:immich_mobile/entities/etag.entity.dart';
 import 'package:immich_mobile/entities/exif_info.entity.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
 import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
 import 'package:immich_mobile/services/hash.service.dart';
 import 'package:immich_mobile/utils/async_mutex.dart';
 import 'package:immich_mobile/extensions/collection_extensions.dart';
@@ -17,19 +19,23 @@ import 'package:immich_mobile/utils/diff.dart';
 import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
 import 'package:openapi/api.dart';
-import 'package:photo_manager/photo_manager.dart';
 
 final syncServiceProvider = Provider(
-  (ref) => SyncService(ref.watch(dbProvider), ref.watch(hashServiceProvider)),
+  (ref) => SyncService(
+    ref.watch(dbProvider),
+    ref.watch(hashServiceProvider),
+    ref.watch(albumMediaRepositoryProvider),
+  ),
 );
 
 class SyncService {
   final Isar _db;
   final HashService _hashService;
+  final IAlbumMediaRepository _albumMediaRepository;
   final AsyncMutex _lock = AsyncMutex();
   final Logger _log = Logger('SyncService');
 
-  SyncService(this._db, this._hashService);
+  SyncService(this._db, this._hashService, this._albumMediaRepository);
 
   // public methods:
 
@@ -68,7 +74,7 @@ class SyncService {
   /// Syncs all device albums and their assets to the database
   /// Returns `true` if there were any changes
   Future<bool> syncLocalAlbumAssetsToDb(
-    List<AssetPathEntity> onDevice, [
+    List<Album> onDevice, [
     Set<String>? excludedAssets,
   ]) =>
       _lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets));
@@ -492,7 +498,7 @@ class SyncService {
   /// Syncs all device albums and their assets to the database
   /// Returns `true` if there were any changes
   Future<bool> _syncLocalAlbumAssetsToDb(
-    List<AssetPathEntity> onDevice, [
+    List<Album> onDevice, [
     Set<String>? excludedAssets,
   ]) async {
     onDevice.sort((a, b) => a.id.compareTo(b.id));
@@ -504,16 +510,15 @@ class SyncService {
     final bool anyChanges = await diffSortedLists(
       onDevice,
       inDb,
-      compare: (AssetPathEntity a, Album b) => a.id.compareTo(b.localId!),
-      both: (AssetPathEntity ape, Album album) => _syncAlbumInDbAndOnDevice(
-        ape,
-        album,
+      compare: (Album a, Album b) => a.localId!.compareTo(b.localId!),
+      both: (Album a, Album b) => _syncAlbumInDbAndOnDevice(
+        a,
+        b,
         deleteCandidates,
         existing,
         excludedAssets,
       ),
-      onlyFirst: (AssetPathEntity ape) =>
-          _addAlbumFromDevice(ape, existing, excludedAssets),
+      onlyFirst: (Album a) => _addAlbumFromDevice(a, existing, excludedAssets),
       onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates),
     );
     _log.fine(
@@ -541,58 +546,65 @@ class SyncService {
   /// returns `true` if there were any changes
   /// Accumulates asset candidates to delete and those already existing in DB
   Future<bool> _syncAlbumInDbAndOnDevice(
-    AssetPathEntity ape,
-    Album album,
+    Album deviceAlbum,
+    Album dbAlbum,
     List<Asset> deleteCandidates,
     List<Asset> existing, [
     Set<String>? excludedAssets,
     bool forceRefresh = false,
   ]) async {
-    if (!forceRefresh && !await _hasAssetPathEntityChanged(ape, album)) {
-      _log.fine("Local album ${ape.name} has not changed. Skipping sync.");
+    if (!forceRefresh && !await _hasAlbumChangeOnDevice(deviceAlbum, dbAlbum)) {
+      _log.fine(
+        "Local album ${deviceAlbum.name} has not changed. Skipping sync.",
+      );
       return false;
     }
     if (!forceRefresh &&
         excludedAssets == null &&
-        await _syncDeviceAlbumFast(ape, album)) {
+        await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) {
       return true;
     }
 
     // general case, e.g. some assets have been deleted or there are excluded albums on iOS
-    final inDb = await album.assets
+    final inDb = await dbAlbum.assets
         .filter()
         .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
         .sortByChecksum()
         .findAll();
     assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
-    final int assetCountOnDevice = await ape.assetCountAsync;
-    final List<Asset> onDevice =
-        await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
+    final int assetCountOnDevice =
+        await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
+    final List<Asset> onDevice = await _hashService.getHashedAssets(
+      deviceAlbum,
+      excludedAssets: excludedAssets,
+    );
     _removeDuplicates(onDevice);
     // _removeDuplicates sorts `onDevice` by checksum
     final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb);
     if (toAdd.isEmpty &&
         toUpdate.isEmpty &&
         toDelete.isEmpty &&
-        album.name == ape.name &&
-        ape.lastModified != null &&
-        album.modifiedAt.isAtSameMomentAs(ape.lastModified!)) {
+        dbAlbum.name == deviceAlbum.name &&
+        dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) {
       // changes only affeted excluded albums
       _log.fine(
-        "Only excluded assets in local album ${ape.name} changed. Stopping sync.",
+        "Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync.",
       );
       if (assetCountOnDevice !=
-          _db.eTags.getByIdSync(ape.eTagKeyAssetCount)?.assetCount) {
+          _db.eTags.getByIdSync(deviceAlbum.eTagKeyAssetCount)?.assetCount) {
         await _db.writeTxn(
           () => _db.eTags.put(
-            ETag(id: ape.eTagKeyAssetCount, assetCount: assetCountOnDevice),
+            ETag(
+              id: deviceAlbum.eTagKeyAssetCount,
+              assetCount: assetCountOnDevice,
+            ),
           ),
         );
       }
       return false;
     }
     _log.fine(
-      "Syncing local album ${ape.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete",
+      "Syncing local album ${deviceAlbum.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete",
     );
     final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
     _log.fine(
@@ -600,28 +612,31 @@ class SyncService {
     );
     deleteCandidates.addAll(toDelete);
     existing.addAll(existingInDb);
-    album.name = ape.name;
-    album.modifiedAt = ape.lastModified ?? DateTime.now();
-    if (album.thumbnail.value != null &&
-        toDelete.contains(album.thumbnail.value)) {
-      album.thumbnail.value = null;
+    dbAlbum.name = deviceAlbum.name;
+    dbAlbum.modifiedAt = deviceAlbum.modifiedAt;
+    if (dbAlbum.thumbnail.value != null &&
+        toDelete.contains(dbAlbum.thumbnail.value)) {
+      dbAlbum.thumbnail.value = null;
     }
     try {
       await _db.writeTxn(() async {
         await _db.assets.putAll(updated);
         await _db.assets.putAll(toUpdate);
-        await album.assets
+        await dbAlbum.assets
             .update(link: existingInDb + updated, unlink: toDelete);
-        await _db.albums.put(album);
-        album.thumbnail.value ??= await album.assets.filter().findFirst();
-        await album.thumbnail.save();
+        await _db.albums.put(dbAlbum);
+        dbAlbum.thumbnail.value ??= await dbAlbum.assets.filter().findFirst();
+        await dbAlbum.thumbnail.save();
         await _db.eTags.put(
-          ETag(id: ape.eTagKeyAssetCount, assetCount: assetCountOnDevice),
+          ETag(
+            id: deviceAlbum.eTagKeyAssetCount,
+            assetCount: assetCountOnDevice,
+          ),
         );
       });
-      _log.info("Synced changes of local album ${ape.name} to DB");
+      _log.info("Synced changes of local album ${deviceAlbum.name} to DB");
     } on IsarError catch (e) {
-      _log.severe("Failed to update synced album ${ape.name} in DB", e);
+      _log.severe("Failed to update synced album ${deviceAlbum.name} in DB", e);
     }
 
     return true;
@@ -629,45 +644,45 @@ class SyncService {
 
   /// fast path for common case: only new assets were added to device album
   /// returns `true` if successfull, else `false`
-  Future<bool> _syncDeviceAlbumFast(AssetPathEntity ape, Album album) async {
-    if (!(ape.lastModified ?? DateTime.now()).isAfter(album.modifiedAt)) {
+  Future<bool> _syncDeviceAlbumFast(Album deviceAlbum, Album dbAlbum) async {
+    if (!deviceAlbum.modifiedAt.isAfter(dbAlbum.modifiedAt)) {
       return false;
     }
-    final int totalOnDevice = await ape.assetCountAsync;
+    final int totalOnDevice =
+        await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
     final int lastKnownTotal =
-        (await _db.eTags.getById(ape.eTagKeyAssetCount))?.assetCount ?? 0;
-    final AssetPathEntity? modified = totalOnDevice > lastKnownTotal
-        ? await ape.fetchPathProperties(
-            filterOptionGroup: FilterOptionGroup(
-              updateTimeCond: DateTimeCond(
-                min: album.modifiedAt.add(const Duration(seconds: 1)),
-                max: ape.lastModified ?? DateTime.now(),
-              ),
-            ),
-          )
-        : null;
-    if (modified == null) {
+        (await _db.eTags.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount ??
+            0;
+    if (totalOnDevice <= lastKnownTotal) {
       return false;
     }
-    final List<Asset> newAssets = await _hashService.getHashedAssets(modified);
+    final List<Asset> newAssets = await _hashService.getHashedAssets(
+      deviceAlbum,
+      modifiedFrom: dbAlbum.modifiedAt.add(const Duration(seconds: 1)),
+      modifiedUntil: deviceAlbum.modifiedAt,
+    );
 
     if (totalOnDevice != lastKnownTotal + newAssets.length) {
       return false;
     }
-    album.modifiedAt = ape.lastModified ?? DateTime.now();
+    dbAlbum.modifiedAt = deviceAlbum.modifiedAt;
     _removeDuplicates(newAssets);
     final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets);
     try {
       await _db.writeTxn(() async {
         await _db.assets.putAll(updated);
-        await album.assets.update(link: existingInDb + updated);
-        await _db.albums.put(album);
-        await _db.eTags
-            .put(ETag(id: ape.eTagKeyAssetCount, assetCount: totalOnDevice));
+        await dbAlbum.assets.update(link: existingInDb + updated);
+        await _db.albums.put(dbAlbum);
+        await _db.eTags.put(
+          ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice),
+        );
       });
-      _log.info("Fast synced local album ${ape.name} to DB");
+      _log.info("Fast synced local album ${deviceAlbum.name} to DB");
     } on IsarError catch (e) {
-      _log.severe("Failed to fast sync local album ${ape.name} to DB", e);
+      _log.severe(
+        "Failed to fast sync local album ${deviceAlbum.name} to DB",
+        e,
+      );
       return false;
     }
 
@@ -677,14 +692,15 @@ class SyncService {
   /// Adds a new album from the device to the database and Accumulates all
   /// assets already existing in the database to the list of `existing` assets
   Future<void> _addAlbumFromDevice(
-    AssetPathEntity ape,
+    Album album,
     List<Asset> existing, [
     Set<String>? excludedAssets,
   ]) async {
-    _log.info("Syncing a new local album to DB: ${ape.name}");
-    final Album a = Album.local(ape);
-    final assets =
-        await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
+    _log.info("Syncing a new local album to DB: ${album.name}");
+    final assets = await _hashService.getHashedAssets(
+      album,
+      excludedAssets: excludedAssets,
+    );
     _removeDuplicates(assets);
     final (existingInDb, updated) = await _linkWithExistingFromDb(assets);
     _log.info(
@@ -692,15 +708,15 @@ class SyncService {
     );
     await upsertAssetsWithExif(updated);
     existing.addAll(existingInDb);
-    a.assets.addAll(existingInDb);
-    a.assets.addAll(updated);
+    album.assets.addAll(existingInDb);
+    album.assets.addAll(updated);
     final thumb = existingInDb.firstOrNull ?? updated.firstOrNull;
-    a.thumbnail.value = thumb;
+    album.thumbnail.value = thumb;
     try {
-      await _db.writeTxn(() => _db.albums.store(a));
-      _log.info("Added a new local album to DB: ${ape.name}");
+      await _db.writeTxn(() => _db.albums.store(album));
+      _log.info("Added a new local album to DB: ${album.name}");
     } on IsarError catch (e) {
-      _log.severe("Failed to add new local album ${ape.name} to DB", e);
+      _log.severe("Failed to add new local album ${album.name} to DB", e);
     }
   }
 
@@ -798,12 +814,15 @@ class SyncService {
   }
 
   /// returns `true` if the albums differ on the surface
-  Future<bool> _hasAssetPathEntityChanged(AssetPathEntity a, Album b) async {
-    return a.name != b.name ||
-        a.lastModified == null ||
-        !a.lastModified!.isAtSameMomentAs(b.modifiedAt) ||
-        await a.assetCountAsync !=
-            (await _db.eTags.getById(a.eTagKeyAssetCount))?.assetCount;
+  Future<bool> _hasAlbumChangeOnDevice(
+    Album deviceAlbum,
+    Album dbAlbum,
+  ) async {
+    return deviceAlbum.name != dbAlbum.name ||
+        !deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
+        await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) !=
+            (await _db.eTags.getById(deviceAlbum.eTagKeyAssetCount))
+                ?.assetCount;
   }
 
   Future<bool> _removeAllLocalAlbumsAndAssets() async {
diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart
index 0c9cd2d89d..7b04855809 100644
--- a/mobile/lib/widgets/backup/album_info_card.dart
+++ b/mobile/lib/widgets/backup/album_info_card.dart
@@ -183,23 +183,13 @@ class AlbumInfoCard extends HookConsumerWidget {
                         ),
                         Padding(
                           padding: const EdgeInsets.only(top: 2.0),
-                          child: FutureBuilder(
-                            builder: ((context, snapshot) {
-                              if (snapshot.hasData) {
-                                return Text(
-                                  snapshot.data.toString() +
-                                      (album.isAll
-                                          ? " (${'backup_all'.tr()})"
-                                          : ""),
-                                  style: TextStyle(
-                                    fontSize: 12,
-                                    color: Colors.grey[600],
-                                  ),
-                                );
-                              }
-                              return const Text("0");
-                            }),
-                            future: album.assetCount,
+                          child: Text(
+                            album.assetCount.toString() +
+                                (album.isAll ? " (${'backup_all'.tr()})" : ""),
+                            style: TextStyle(
+                              fontSize: 12,
+                              color: Colors.grey[600],
+                            ),
                           ),
                         ),
                       ],
@@ -208,7 +198,7 @@ class AlbumInfoCard extends HookConsumerWidget {
                   IconButton(
                     onPressed: () {
                       context.pushRoute(
-                        AlbumPreviewRoute(album: album.albumEntity),
+                        AlbumPreviewRoute(album: album.album),
                       );
                     },
                     icon: Icon(
diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart
index d326bad3e0..a263c004bd 100644
--- a/mobile/lib/widgets/backup/album_info_list_tile.dart
+++ b/mobile/lib/widgets/backup/album_info_list_tile.dart
@@ -1,6 +1,5 @@
 import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
@@ -24,19 +23,10 @@ class AlbumInfoListTile extends HookConsumerWidget {
         ref.watch(backupProvider).selectedBackupAlbums.contains(album);
     final bool isExcluded =
         ref.watch(backupProvider).excludedBackupAlbums.contains(album);
-    final assetCount = useState(0);
     final syncAlbum = ref
         .watch(appSettingsServiceProvider)
         .getSetting(AppSettingsEnum.syncAlbums);
 
-    useEffect(
-      () {
-        album.assetCount.then((value) => assetCount.value = value);
-        return null;
-      },
-      [album],
-    );
-
     buildTileColor() {
       if (isSelected) {
         return context.isDarkTheme
@@ -117,11 +107,11 @@ class AlbumInfoListTile extends HookConsumerWidget {
             fontWeight: FontWeight.bold,
           ),
         ),
-        subtitle: Text(assetCount.value.toString()),
+        subtitle: Text(album.assetCount.toString()),
         trailing: IconButton(
           onPressed: () {
             context.pushRoute(
-              AlbumPreviewRoute(album: album.albumEntity),
+              AlbumPreviewRoute(album: album.album),
             );
           },
           icon: Icon(
diff --git a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart
index 8e58905aaa..f2f84e271f 100644
--- a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart
+++ b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart
@@ -2,18 +2,19 @@ import 'dart:io';
 
 import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/theme_extensions.dart';
 import 'package:immich_mobile/models/backup/backup_state.model.dart';
 import 'package:immich_mobile/providers/backup/backup.provider.dart';
 import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
 import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
+import 'package:immich_mobile/repositories/asset_media.repository.dart';
 import 'package:immich_mobile/routing/router.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
 
 class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
   const CurrentUploadingAssetInfoBox({super.key});
@@ -148,17 +149,6 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
       );
     }
 
-    buildAssetThumbnail() async {
-      var assetEntity = await AssetEntity.fromId(asset.id);
-
-      if (assetEntity != null) {
-        return assetEntity.thumbnailDataWithSize(
-          const ThumbnailSize(500, 500),
-          quality: 100,
-        );
-      }
-    }
-
     buildiCloudDownloadProgerssBar() {
       if (asset.iCloudAsset != null && asset.iCloudAsset!) {
         return Padding(
@@ -239,8 +229,8 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
       );
     }
 
-    return FutureBuilder<Uint8List?>(
-      future: buildAssetThumbnail(),
+    return FutureBuilder<Asset?>(
+      future: ref.read(assetMediaRepositoryProvider).get(asset.id),
       builder: (context, thumbnail) => ListTile(
         isThreeLine: true,
         leading: AnimatedCrossFade(
@@ -250,9 +240,8 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
             child: thumbnail.hasData
                 ? ClipRRect(
                     borderRadius: BorderRadius.circular(5),
-                    child: Image.memory(
-                      thumbnail.data!,
-                      fit: BoxFit.cover,
+                    child: ImmichThumbnail(
+                      asset: thumbnail.data,
                       width: 50,
                       height: 50,
                     ),
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index c9493f6490..7fe33c3270 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -836,6 +836,13 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.2.1+1"
+  immich_mobile_immich_lint:
+    dependency: "direct dev"
+    description:
+      path: immich_lint
+      relative: true
+    source: path
+    version: "0.0.0"
   integration_test:
     dependency: "direct dev"
     description: flutter
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index 0061f563d2..8787fd8565 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -94,10 +94,12 @@ dev_dependencies:
   isar_generator: ^3.1.0+1
   integration_test:
     sdk: flutter
-  custom_lint: ^0.6.0
+  custom_lint: ^0.6.4
   riverpod_lint: ^2.3.7
   riverpod_generator: ^2.3.9
   mocktail: ^1.0.3
+  immich_mobile_immich_lint:
+    path: './immich_lint'
 
 flutter:
   uses-material-design: true
diff --git a/mobile/test/modules/shared/shared_mocks.dart b/mobile/test/modules/shared/shared_mocks.dart
index a2aa7b2617..013232da3e 100644
--- a/mobile/test/modules/shared/shared_mocks.dart
+++ b/mobile/test/modules/shared/shared_mocks.dart
@@ -1,11 +1,8 @@
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/entities/user.entity.dart';
 import 'package:immich_mobile/providers/user.provider.dart';
-import 'package:immich_mobile/services/hash.service.dart';
 import 'package:mocktail/mocktail.dart';
 
-class MockHashService extends Mock implements HashService {}
-
 class MockCurrentUserProvider extends StateNotifier<User?>
     with Mock
     implements CurrentUserProvider {
diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart
index 07437289be..5ae0fb3c52 100644
--- a/mobile/test/modules/shared/sync_service_test.dart
+++ b/mobile/test/modules/shared/sync_service_test.dart
@@ -7,8 +7,9 @@ import 'package:immich_mobile/services/immich_logger.service.dart';
 import 'package:immich_mobile/services/sync.service.dart';
 import 'package:isar/isar.dart';
 
+import '../../repository.mocks.dart';
+import '../../service.mocks.dart';
 import '../../test_utils.dart';
-import 'shared_mocks.dart';
 
 void main() {
   Asset makeAsset({
@@ -38,6 +39,8 @@ void main() {
   group('Test SyncService grouped', () {
     late final Isar db;
     final MockHashService hs = MockHashService();
+    final MockAlbumMediaRepository albumMediaRepository =
+        MockAlbumMediaRepository();
     final owner = User(
       id: "1",
       updatedAt: DateTime.now(),
@@ -67,7 +70,7 @@ void main() {
       });
     });
     test('test inserting existing assets', () async {
-      SyncService s = SyncService(db, hs);
+      SyncService s = SyncService(db, hs, albumMediaRepository);
       final List<Asset> remoteAssets = [
         makeAsset(checksum: "a", remoteId: "0-1"),
         makeAsset(checksum: "b", remoteId: "2-1"),
@@ -85,7 +88,7 @@ void main() {
     });
 
     test('test inserting new assets', () async {
-      SyncService s = SyncService(db, hs);
+      SyncService s = SyncService(db, hs, albumMediaRepository);
       final List<Asset> remoteAssets = [
         makeAsset(checksum: "a", remoteId: "0-1"),
         makeAsset(checksum: "b", remoteId: "2-1"),
@@ -106,7 +109,7 @@ void main() {
     });
 
     test('test syncing duplicate assets', () async {
-      SyncService s = SyncService(db, hs);
+      SyncService s = SyncService(db, hs, albumMediaRepository);
       final List<Asset> remoteAssets = [
         makeAsset(checksum: "a", remoteId: "0-1"),
         makeAsset(checksum: "b", remoteId: "1-1"),
@@ -154,7 +157,7 @@ void main() {
     });
 
     test('test efficient sync', () async {
-      SyncService s = SyncService(db, hs);
+      SyncService s = SyncService(db, hs, albumMediaRepository);
       final List<Asset> toUpsert = [
         makeAsset(checksum: "a", remoteId: "0-1"), // changed
         makeAsset(checksum: "f", remoteId: "0-2"), // new
diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart
index e54d82739e..798f6f420a 100644
--- a/mobile/test/repository.mocks.dart
+++ b/mobile/test/repository.mocks.dart
@@ -1,6 +1,9 @@
 import 'package:immich_mobile/interfaces/album.interface.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
 import 'package:immich_mobile/interfaces/asset.interface.dart';
+import 'package:immich_mobile/interfaces/asset_media.interface.dart';
 import 'package:immich_mobile/interfaces/backup.interface.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
 import 'package:immich_mobile/interfaces/user.interface.dart';
 import 'package:mocktail/mocktail.dart';
 
@@ -11,3 +14,9 @@ class MockAssetRepository extends Mock implements IAssetRepository {}
 class MockUserRepository extends Mock implements IUserRepository {}
 
 class MockBackupRepository extends Mock implements IBackupRepository {}
+
+class MockAlbumMediaRepository extends Mock implements IAlbumMediaRepository {}
+
+class MockAssetMediaRepository extends Mock implements IAssetMediaRepository {}
+
+class MockFileMediaRepository extends Mock implements IFileMediaRepository {}
diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart
index ba4c129e5c..bd5e8bee23 100644
--- a/mobile/test/service.mocks.dart
+++ b/mobile/test/service.mocks.dart
@@ -1,4 +1,5 @@
 import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/services/hash.service.dart';
 import 'package:immich_mobile/services/sync.service.dart';
 import 'package:immich_mobile/services/user.service.dart';
 import 'package:mocktail/mocktail.dart';
@@ -8,3 +9,5 @@ class MockApiService extends Mock implements ApiService {}
 class MockUserService extends Mock implements UserService {}
 
 class MockSyncService extends Mock implements SyncService {}
+
+class MockHashService extends Mock implements HashService {}
diff --git a/mobile/test/services/album.service.test.dart b/mobile/test/services/album.service_test.dart
similarity index 64%
rename from mobile/test/services/album.service.test.dart
rename to mobile/test/services/album.service_test.dart
index 790a0eba35..47f9c005a7 100644
--- a/mobile/test/services/album.service.test.dart
+++ b/mobile/test/services/album.service_test.dart
@@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
 import 'package:immich_mobile/entities/backup_album.entity.dart';
 import 'package:immich_mobile/services/album.service.dart';
 import 'package:mocktail/mocktail.dart';
+import '../fixtures/album.stub.dart';
 import '../repository.mocks.dart';
 import '../service.mocks.dart';
 
@@ -14,6 +15,7 @@ void main() {
   late MockAssetRepository assetRepository;
   late MockUserRepository userRepository;
   late MockBackupRepository backupRepository;
+  late MockAlbumMediaRepository albumMediaRepository;
 
   setUp(() {
     apiService = MockApiService();
@@ -23,6 +25,7 @@ void main() {
     assetRepository = MockAssetRepository();
     userRepository = MockUserRepository();
     backupRepository = MockBackupRepository();
+    albumMediaRepository = MockAlbumMediaRepository();
 
     sut = AlbumService(
       apiService,
@@ -32,6 +35,7 @@ void main() {
       assetRepository,
       userRepository,
       backupRepository,
+      albumMediaRepository,
     );
   });
 
@@ -48,5 +52,22 @@ void main() {
       expect(result, false);
       verify(() => syncService.removeAllLocalAlbumsAndAssets());
     });
+
+    test('one selected albums, two on device', () async {
+      when(() => backupRepository.getIdsBySelection(BackupSelection.exclude))
+          .thenAnswer((_) async => []);
+      when(() => backupRepository.getIdsBySelection(BackupSelection.select))
+          .thenAnswer((_) async => [AlbumStub.oneAsset.localId!]);
+      when(() => albumMediaRepository.getAll())
+          .thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]);
+      when(() => syncService.syncLocalAlbumAssetsToDb(any(), any()))
+          .thenAnswer((_) async => true);
+      final result = await sut.refreshDeviceAlbums();
+      expect(result, true);
+      verify(
+        () => syncService.syncLocalAlbumAssetsToDb([AlbumStub.oneAsset], null),
+      ).called(1);
+      verifyNoMoreInteractions(syncService);
+    });
   });
 }