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