1 package expo.modules.updates.db.dao
2 
3 import androidx.room.*
4 import expo.modules.updates.db.entity.AssetEntity
5 import expo.modules.updates.db.entity.UpdateAssetEntity
6 import expo.modules.updates.db.entity.UpdateEntity
7 import java.util.*
8 
9 /**
10  * Utility class for accessing and modifying data in SQLite relating to assets.
11  */
12 @Dao
13 abstract class AssetDao {
14   /**
15    * for private use only
16    * must be marked public for Room
17    * so we use the underscore to discourage use
18    */
19   @Insert(onConflict = OnConflictStrategy.REPLACE)
_insertAssetnull20   abstract fun _insertAsset(asset: AssetEntity): Long
21 
22   @Insert(onConflict = OnConflictStrategy.REPLACE)
23   abstract fun _insertUpdateAsset(updateAsset: UpdateAssetEntity)
24 
25   @Query("UPDATE updates SET launch_asset_id = :assetId WHERE id = :updateId;")
26   abstract fun _setUpdateLaunchAsset(assetId: Long, updateId: UUID)
27 
28   @Query("UPDATE assets SET marked_for_deletion = 1;")
29   abstract fun _markAllAssetsForDeletion()
30 
31   @Query(
32     "UPDATE assets SET marked_for_deletion = 0 WHERE id IN (" +
33       " SELECT asset_id" +
34       " FROM updates_assets" +
35       " INNER JOIN updates ON updates_assets.update_id = updates.id" +
36       " WHERE updates.keep);"
37   )
38   abstract fun _unmarkUsedAssetsFromDeletion()
39 
40   @Query(
41     "UPDATE assets SET marked_for_deletion = 0 WHERE relative_path IN (" +
42       " SELECT relative_path" +
43       " FROM assets" +
44       " WHERE marked_for_deletion = 0);"
45   )
46   abstract fun _unmarkDuplicateUsedAssetsFromDeletion()
47 
48   @Query("SELECT * FROM assets WHERE marked_for_deletion = 1;")
49   abstract fun _loadAssetsMarkedForDeletion(): List<AssetEntity>
50 
51   @Query("DELETE FROM assets WHERE marked_for_deletion = 1;")
52   abstract fun _deleteAssetsMarkedForDeletion()
53 
54   @Query("SELECT * FROM assets WHERE `key` = :key LIMIT 1;")
55   abstract fun _loadAssetWithKey(key: String?): List<AssetEntity>
56 
57   /**
58    * for public use
59    */
60   @Query("SELECT * FROM assets;")
61   abstract fun loadAllAssets(): List<AssetEntity>
62 
63   @Query(
64     "SELECT assets.*" +
65       " FROM assets" +
66       " INNER JOIN updates_assets ON updates_assets.asset_id = assets.id" +
67       " INNER JOIN updates ON updates_assets.update_id = updates.id" +
68       " WHERE updates.id = :id;"
69   )
70   abstract fun loadAssetsForUpdate(id: UUID): List<AssetEntity>
71 
72   @Update
73   abstract fun updateAsset(assetEntity: AssetEntity)
74 
75   @Transaction
76   open fun insertAssets(assets: List<AssetEntity>, update: UpdateEntity) {
77     for (asset in assets) {
78       val assetId = _insertAsset(asset)
79       _insertUpdateAsset(UpdateAssetEntity(update.id, assetId))
80       if (asset.isLaunchAsset) {
81         _setUpdateLaunchAsset(assetId, update.id)
82       }
83     }
84   }
85 
loadAssetWithKeynull86   fun loadAssetWithKey(key: String?): AssetEntity? {
87     val assets = _loadAssetWithKey(key)
88     return if (assets.isNotEmpty()) {
89       assets[0]
90     } else null
91   }
92 
mergeAndUpdateAssetnull93   fun mergeAndUpdateAsset(existingEntity: AssetEntity, newEntity: AssetEntity) {
94     // if the existing entry came from an embedded manifest, it may not have a URL in the database
95     var shouldUpdate = false
96     if (newEntity.url != null && (existingEntity.url == null || newEntity.url != existingEntity.url)) {
97       existingEntity.url = newEntity.url
98       shouldUpdate = true
99     }
100 
101     val newEntityExtraRequestHeaders = newEntity.extraRequestHeaders
102     if (newEntityExtraRequestHeaders != null &&
103       (existingEntity.extraRequestHeaders == null || newEntityExtraRequestHeaders != existingEntity.extraRequestHeaders)
104     ) {
105       existingEntity.extraRequestHeaders = newEntity.extraRequestHeaders
106       shouldUpdate = true
107     }
108 
109     if (shouldUpdate) {
110       updateAsset(existingEntity)
111     }
112 
113     // we need to keep track of whether the calling class expects this asset to be the launch asset
114     existingEntity.isLaunchAsset = newEntity.isLaunchAsset
115     // some fields on the asset entity are not stored in the database but might still be used by application code
116     existingEntity.embeddedAssetFilename = newEntity.embeddedAssetFilename
117     existingEntity.resourcesFilename = newEntity.resourcesFilename
118     existingEntity.resourcesFolder = newEntity.resourcesFolder
119     existingEntity.scale = newEntity.scale
120     existingEntity.scales = newEntity.scales
121   }
122 
123   @Transaction
addExistingAssetToUpdatenull124   open fun addExistingAssetToUpdate(
125     update: UpdateEntity,
126     asset: AssetEntity,
127     isLaunchAsset: Boolean
128   ): Boolean {
129     val existingAssetEntry = loadAssetWithKey(asset.key) ?: return false
130     val assetId = existingAssetEntry.id
131     _insertUpdateAsset(UpdateAssetEntity(update.id, assetId))
132     if (isLaunchAsset) {
133       _setUpdateLaunchAsset(assetId, update.id)
134     }
135     return true
136   }
137 
138   @Transaction
deleteUnusedAssetsnull139   open fun deleteUnusedAssets(): List<AssetEntity> {
140     // the simplest way to mark the assets we want to delete
141     // is to mark all assets for deletion, then go back and unmark
142     // those assets in updates we want to keep
143     // this is safe since this is a transaction and will be rolled back upon failure
144     _markAllAssetsForDeletion()
145     _unmarkUsedAssetsFromDeletion()
146     // check for duplicate rows representing a single file on disk
147     _unmarkDuplicateUsedAssetsFromDeletion()
148     val deletedAssets = _loadAssetsMarkedForDeletion()
149     _deleteAssetsMarkedForDeletion()
150     return deletedAssets
151   }
152 }
153