react-native-instantpay-code-push 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/InstantpayCodePush.podspec +20 -0
- package/LICENSE +20 -0
- package/README.md +158 -0
- package/android/build.gradle +91 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/instantpaycodepush/BundleFileStorageService.kt +835 -0
- package/android/src/main/java/com/instantpaycodepush/BundleMetadata.kt +249 -0
- package/android/src/main/java/com/instantpaycodepush/CommonHelper.kt +39 -0
- package/android/src/main/java/com/instantpaycodepush/DecompressService.kt +85 -0
- package/android/src/main/java/com/instantpaycodepush/DecompressionStrategy.kt +24 -0
- package/android/src/main/java/com/instantpaycodepush/FileManagerService.kt +105 -0
- package/android/src/main/java/com/instantpaycodepush/HashUtils.kt +50 -0
- package/android/src/main/java/com/instantpaycodepush/InstantpayCodePushModule.kt +182 -0
- package/android/src/main/java/com/instantpaycodepush/InstantpayCodePushPackage.kt +33 -0
- package/android/src/main/java/com/instantpaycodepush/IpayCodePush.kt +101 -0
- package/android/src/main/java/com/instantpaycodepush/IpayCodePushException.kt +135 -0
- package/android/src/main/java/com/instantpaycodepush/IpayCodePushImpl.kt +329 -0
- package/android/src/main/java/com/instantpaycodepush/OkHttpDownloadService.kt +283 -0
- package/android/src/main/java/com/instantpaycodepush/ReactIntegrationManager.kt +141 -0
- package/android/src/main/java/com/instantpaycodepush/ReactIntegrationManagerBase.kt +35 -0
- package/android/src/main/java/com/instantpaycodepush/SignatureVerifier.kt +354 -0
- package/android/src/main/java/com/instantpaycodepush/VersionedPreferencesService.kt +70 -0
- package/android/src/main/java/com/instantpaycodepush/ZipDecompressionStrategy.kt +198 -0
- package/ios/InstantpayCodePush.h +5 -0
- package/ios/InstantpayCodePush.mm +21 -0
- package/lib/module/DefaultResolver.js +34 -0
- package/lib/module/DefaultResolver.js.map +1 -0
- package/lib/module/NativeInstantpayCodePush.js +5 -0
- package/lib/module/NativeInstantpayCodePush.js.map +1 -0
- package/lib/module/checkForUpdate.js +68 -0
- package/lib/module/checkForUpdate.js.map +1 -0
- package/lib/module/error.js +137 -0
- package/lib/module/error.js.map +1 -0
- package/lib/module/fetchUpdateInfo.js +36 -0
- package/lib/module/fetchUpdateInfo.js.map +1 -0
- package/lib/module/global.d.js +8 -0
- package/lib/module/global.d.js.map +1 -0
- package/lib/module/hooks/useEventCallback.js +13 -0
- package/lib/module/hooks/useEventCallback.js.map +1 -0
- package/lib/module/index.js +291 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native.js +233 -0
- package/lib/module/native.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/store.js +53 -0
- package/lib/module/store.js.map +1 -0
- package/lib/module/types.js +62 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/wrap.js +171 -0
- package/lib/module/wrap.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/DefaultResolver.d.ts +10 -0
- package/lib/typescript/src/DefaultResolver.d.ts.map +1 -0
- package/lib/typescript/src/NativeInstantpayCodePush.d.ts +100 -0
- package/lib/typescript/src/NativeInstantpayCodePush.d.ts.map +1 -0
- package/lib/typescript/src/checkForUpdate.d.ts +29 -0
- package/lib/typescript/src/checkForUpdate.d.ts.map +1 -0
- package/lib/typescript/src/error.d.ts +124 -0
- package/lib/typescript/src/error.d.ts.map +1 -0
- package/lib/typescript/src/fetchUpdateInfo.d.ts +8 -0
- package/lib/typescript/src/fetchUpdateInfo.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useEventCallback.d.ts +5 -0
- package/lib/typescript/src/hooks/useEventCallback.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +203 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/native.d.ts +128 -0
- package/lib/typescript/src/native.d.ts.map +1 -0
- package/lib/typescript/src/store.d.ts +11 -0
- package/lib/typescript/src/store.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +174 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/wrap.d.ts +179 -0
- package/lib/typescript/src/wrap.d.ts.map +1 -0
- package/package.json +174 -0
- package/src/DefaultResolver.ts +36 -0
- package/src/NativeInstantpayCodePush.ts +111 -0
- package/src/checkForUpdate.ts +122 -0
- package/src/error.ts +159 -0
- package/src/fetchUpdateInfo.ts +47 -0
- package/src/global.d.ts +23 -0
- package/src/hooks/useEventCallback.ts +30 -0
- package/src/index.tsx +379 -0
- package/src/native.ts +280 -0
- package/src/store.ts +69 -0
- package/src/types.ts +227 -0
- package/src/wrap.tsx +384 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
package com.instantpaycodepush
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import org.json.JSONArray
|
|
5
|
+
import org.json.JSONObject
|
|
6
|
+
import java.io.File
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Bundle metadata for managing stable/staging bundles and verification state
|
|
11
|
+
*/
|
|
12
|
+
data class BundleMetadata(
|
|
13
|
+
val schema: String = SCHEMA_VERSION,
|
|
14
|
+
val isolationKey: String? = null,
|
|
15
|
+
val stableBundleId: String? = null,
|
|
16
|
+
val stagingBundleId: String? = null,
|
|
17
|
+
val verificationPending: Boolean = false,
|
|
18
|
+
val verificationAttemptedAt: Long? = null,
|
|
19
|
+
val stagingExecutionCount: Int? = null,
|
|
20
|
+
val updatedAt: Long = System.currentTimeMillis(),
|
|
21
|
+
){
|
|
22
|
+
|
|
23
|
+
companion object {
|
|
24
|
+
|
|
25
|
+
private const val CLASS_TAG = "*BundleMetadata"
|
|
26
|
+
const val SCHEMA_VERSION = "metadata-v1"
|
|
27
|
+
const val METADATA_FILENAME = "metadata.json"
|
|
28
|
+
|
|
29
|
+
fun fromJson(json: JSONObject): BundleMetadata =
|
|
30
|
+
BundleMetadata(
|
|
31
|
+
schema = json.optString("schema", SCHEMA_VERSION),
|
|
32
|
+
isolationKey =
|
|
33
|
+
if (json.has("isolationKey") && !json.isNull("isolationKey")) {
|
|
34
|
+
json.getString("isolationKey").takeIf { it.isNotEmpty() }
|
|
35
|
+
} else {
|
|
36
|
+
null
|
|
37
|
+
},
|
|
38
|
+
stableBundleId =
|
|
39
|
+
if (json.has("stableBundleId") && !json.isNull("stableBundleId")) {
|
|
40
|
+
json.getString("stableBundleId").takeIf { it.isNotEmpty() }
|
|
41
|
+
} else {
|
|
42
|
+
null
|
|
43
|
+
},
|
|
44
|
+
stagingBundleId =
|
|
45
|
+
if (json.has("stagingBundleId") && !json.isNull("stagingBundleId")) {
|
|
46
|
+
json.getString("stagingBundleId").takeIf { it.isNotEmpty() }
|
|
47
|
+
} else {
|
|
48
|
+
null
|
|
49
|
+
},
|
|
50
|
+
verificationPending = json.optBoolean("verificationPending", false),
|
|
51
|
+
verificationAttemptedAt =
|
|
52
|
+
if (json.has("verificationAttemptedAt") && !json.isNull("verificationAttemptedAt")) {
|
|
53
|
+
json.getLong("verificationAttemptedAt")
|
|
54
|
+
} else {
|
|
55
|
+
null
|
|
56
|
+
},
|
|
57
|
+
stagingExecutionCount =
|
|
58
|
+
if (json.has("stagingExecutionCount") && !json.isNull("stagingExecutionCount")) {
|
|
59
|
+
json.getInt("stagingExecutionCount")
|
|
60
|
+
} else {
|
|
61
|
+
null
|
|
62
|
+
},
|
|
63
|
+
updatedAt = json.optLong("updatedAt", System.currentTimeMillis()),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
fun loadFromFile(
|
|
67
|
+
file: File,
|
|
68
|
+
expectedIsolationKey: String,
|
|
69
|
+
): BundleMetadata? {
|
|
70
|
+
return try {
|
|
71
|
+
|
|
72
|
+
if (!file.exists()) {
|
|
73
|
+
CommonHelper.logPrint(CLASS_TAG, "Metadata file does not exist: ${file.absolutePath}")
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
val jsonString = file.readText()
|
|
78
|
+
val json = JSONObject(jsonString)
|
|
79
|
+
val metadata = fromJson(json)
|
|
80
|
+
|
|
81
|
+
// Validate isolation key
|
|
82
|
+
val metadataKey = metadata.isolationKey
|
|
83
|
+
if (metadataKey != null) {
|
|
84
|
+
if (metadataKey != expectedIsolationKey) {
|
|
85
|
+
CommonHelper.logPrint(CLASS_TAG, "Isolation key mismatch: expected=$expectedIsolationKey, got=$metadataKey")
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
CommonHelper.logPrint(CLASS_TAG, "Missing isolation key in metadata, treating as invalid")
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
metadata
|
|
94
|
+
} catch (e: Exception) {
|
|
95
|
+
CommonHelper.logPrint(CLASS_TAG, "Failed to load metadata from file $e")
|
|
96
|
+
Log.e(CLASS_TAG, "Failed to load metadata from file", e)
|
|
97
|
+
null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fun toJson(): JSONObject =
|
|
104
|
+
JSONObject().apply {
|
|
105
|
+
put("schema", schema)
|
|
106
|
+
put("isolationKey", isolationKey ?: JSONObject.NULL)
|
|
107
|
+
put("stableBundleId", stableBundleId ?: JSONObject.NULL)
|
|
108
|
+
put("stagingBundleId", stagingBundleId ?: JSONObject.NULL)
|
|
109
|
+
put("verificationPending", verificationPending)
|
|
110
|
+
put("verificationAttemptedAt", verificationAttemptedAt ?: JSONObject.NULL)
|
|
111
|
+
put("stagingExecutionCount", stagingExecutionCount ?: JSONObject.NULL)
|
|
112
|
+
put("updatedAt", updatedAt)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fun saveToFile(file: File): Boolean =
|
|
116
|
+
try {
|
|
117
|
+
file.parentFile?.mkdirs()
|
|
118
|
+
file.writeText(toJson().toString(2))
|
|
119
|
+
CommonHelper.logPrint(CLASS_TAG, "Saved metadata to file: ${file.absolutePath}")
|
|
120
|
+
true
|
|
121
|
+
} catch (e: Exception) {
|
|
122
|
+
CommonHelper.logPrint(CLASS_TAG, "Failed to save metadata to file: $e")
|
|
123
|
+
Log.e(CLASS_TAG, "Failed to save metadata to file", e)
|
|
124
|
+
false
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Entry for a crashed bundle in history
|
|
130
|
+
*/
|
|
131
|
+
data class CrashedBundleEntry(
|
|
132
|
+
val bundleId: String,
|
|
133
|
+
val crashedAt: Long,
|
|
134
|
+
val crashCount: Int = 1,
|
|
135
|
+
){
|
|
136
|
+
companion object {
|
|
137
|
+
fun fromJson(json: JSONObject): CrashedBundleEntry =
|
|
138
|
+
CrashedBundleEntry(
|
|
139
|
+
bundleId = json.getString("bundleId"),
|
|
140
|
+
crashedAt = json.getLong("crashedAt"),
|
|
141
|
+
crashCount = json.optInt("crashCount", 1),
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fun toJson(): JSONObject =
|
|
146
|
+
JSONObject().apply {
|
|
147
|
+
put("bundleId", bundleId)
|
|
148
|
+
put("crashedAt", crashedAt)
|
|
149
|
+
put("crashCount", crashCount)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* History of crashed bundles
|
|
155
|
+
*/
|
|
156
|
+
data class CrashedHistory(
|
|
157
|
+
val bundles: MutableList<CrashedBundleEntry> = mutableListOf(),
|
|
158
|
+
val maxHistorySize: Int = DEFAULT_MAX_HISTORY_SIZE,
|
|
159
|
+
) {
|
|
160
|
+
companion object {
|
|
161
|
+
private const val CLSSS_TAG = "CrashedHistory"
|
|
162
|
+
const val DEFAULT_MAX_HISTORY_SIZE = 10
|
|
163
|
+
const val CRASHED_HISTORY_FILENAME = "crashed-history.json"
|
|
164
|
+
|
|
165
|
+
fun fromJson(json: JSONObject): CrashedHistory {
|
|
166
|
+
val bundlesArray = json.optJSONArray("bundles") ?: JSONArray()
|
|
167
|
+
val bundles = mutableListOf<CrashedBundleEntry>()
|
|
168
|
+
for (i in 0 until bundlesArray.length()) {
|
|
169
|
+
bundles.add(CrashedBundleEntry.fromJson(bundlesArray.getJSONObject(i)))
|
|
170
|
+
}
|
|
171
|
+
return CrashedHistory(
|
|
172
|
+
bundles = bundles,
|
|
173
|
+
maxHistorySize = json.optInt("maxHistorySize", DEFAULT_MAX_HISTORY_SIZE),
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fun loadFromFile(file: File): CrashedHistory {
|
|
178
|
+
return try {
|
|
179
|
+
if (!file.exists()) {
|
|
180
|
+
CommonHelper.logPrint(CLSSS_TAG, "Crashed history file does not exist, returning empty history")
|
|
181
|
+
return CrashedHistory()
|
|
182
|
+
}
|
|
183
|
+
val jsonString = file.readText()
|
|
184
|
+
val json = JSONObject(jsonString)
|
|
185
|
+
fromJson(json)
|
|
186
|
+
} catch (e: Exception) {
|
|
187
|
+
CommonHelper.logPrint(CLSSS_TAG, "Failed to load crashed history from file: $e")
|
|
188
|
+
Log.e(CLSSS_TAG, "Failed to load crashed history from file", e)
|
|
189
|
+
CrashedHistory()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fun toJson(): JSONObject =
|
|
195
|
+
JSONObject().apply {
|
|
196
|
+
val bundlesArray = JSONArray()
|
|
197
|
+
bundles.forEach { bundlesArray.put(it.toJson()) }
|
|
198
|
+
put("bundles", bundlesArray)
|
|
199
|
+
put("maxHistorySize", maxHistorySize)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fun saveToFile(file: File): Boolean =
|
|
203
|
+
try {
|
|
204
|
+
file.parentFile?.mkdirs()
|
|
205
|
+
file.writeText(toJson().toString(2))
|
|
206
|
+
CommonHelper.logPrint(CLSSS_TAG, "Saved crashed history to file: ${file.absolutePath}")
|
|
207
|
+
true
|
|
208
|
+
} catch (e: Exception) {
|
|
209
|
+
CommonHelper.logPrint(CLSSS_TAG, "Failed to save crashed history to file: $e")
|
|
210
|
+
Log.e(CLSSS_TAG, "Failed to save crashed history to file", e)
|
|
211
|
+
false
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fun contains(bundleId: String): Boolean = bundles.any { it.bundleId == bundleId }
|
|
215
|
+
|
|
216
|
+
fun addEntry(bundleId: String) {
|
|
217
|
+
val existingIndex = bundles.indexOfFirst { it.bundleId == bundleId }
|
|
218
|
+
if (existingIndex >= 0) {
|
|
219
|
+
// Update existing entry
|
|
220
|
+
val existing = bundles[existingIndex]
|
|
221
|
+
bundles[existingIndex] =
|
|
222
|
+
existing.copy(
|
|
223
|
+
crashedAt = System.currentTimeMillis(),
|
|
224
|
+
crashCount = existing.crashCount + 1,
|
|
225
|
+
)
|
|
226
|
+
} else {
|
|
227
|
+
// Add new entry
|
|
228
|
+
bundles.add(
|
|
229
|
+
CrashedBundleEntry(
|
|
230
|
+
bundleId = bundleId,
|
|
231
|
+
crashedAt = System.currentTimeMillis(),
|
|
232
|
+
crashCount = 1,
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Trim to max size (keep most recent)
|
|
238
|
+
if (bundles.size > maxHistorySize) {
|
|
239
|
+
bundles.sortBy { it.crashedAt }
|
|
240
|
+
while (bundles.size > maxHistorySize) {
|
|
241
|
+
bundles.removeAt(0)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fun clear() {
|
|
247
|
+
bundles.clear()
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package com.instantpaycodepush
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
|
|
5
|
+
object CommonHelper {
|
|
6
|
+
|
|
7
|
+
const val MAIN_LOG_TAG = "*IpayCodePush -> "
|
|
8
|
+
|
|
9
|
+
const val WARNING_LOG = "WARNING_LOG"
|
|
10
|
+
|
|
11
|
+
const val ERROR_LOG = "ERROR_LOG"
|
|
12
|
+
|
|
13
|
+
fun logPrint(classTag:String, value: String?) {
|
|
14
|
+
if (value == null) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
val fullTagName = "$MAIN_LOG_TAG $classTag"
|
|
19
|
+
|
|
20
|
+
Log.i(fullTagName, value)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fun logPrint(type:String,classTag:String, value: String?) {
|
|
24
|
+
if (value == null) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
val fullTagName = "$MAIN_LOG_TAG $classTag"
|
|
29
|
+
|
|
30
|
+
if(type == "WARNING_LOG"){
|
|
31
|
+
Log.i(fullTagName, value)
|
|
32
|
+
} else if(type == "ERROR_LOG") {
|
|
33
|
+
Log.e(fullTagName, value)
|
|
34
|
+
}
|
|
35
|
+
else{
|
|
36
|
+
Log.i(fullTagName, value)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package com.instantpaycodepush
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import java.io.File
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Unified decompression service that uses Strategy pattern to handle multiple compression formats.
|
|
8
|
+
* Automatically detects format by trying each strategy's validation and delegates to appropriate decompression strategy.
|
|
9
|
+
*/
|
|
10
|
+
class DecompressService {
|
|
11
|
+
|
|
12
|
+
companion object {
|
|
13
|
+
private const val CLASS_TAG = "*DecompressService"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Array of available strategies in order of detection priority
|
|
17
|
+
// Order matters: Try ZIP first (clear magic bytes), then TAR.GZ (GZIP magic bytes), then TAR.BR (fallback)
|
|
18
|
+
private val strategies =
|
|
19
|
+
listOf(
|
|
20
|
+
ZipDecompressionStrategy(),
|
|
21
|
+
//TarGzDecompressionStrategy(),
|
|
22
|
+
//TarBrDecompressionStrategy(),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extracts a compressed file to the destination directory.
|
|
27
|
+
* Automatically detects compression format by trying each strategy's validation.
|
|
28
|
+
* @param filePath Path to the compressed file
|
|
29
|
+
* @param destinationPath Path to the destination directory
|
|
30
|
+
* @param progressCallback Callback for progress updates (0.0 - 1.0)
|
|
31
|
+
* @return true if extraction was successful, false otherwise
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
fun extractZipFile(
|
|
35
|
+
filePath: String,
|
|
36
|
+
destinationPath: String,
|
|
37
|
+
progressCallback: (Double) -> Unit,
|
|
38
|
+
): Boolean {
|
|
39
|
+
// Collect file information for better error messages
|
|
40
|
+
val file = File(filePath)
|
|
41
|
+
val fileName = file.name
|
|
42
|
+
val fileSize = if (file.exists()) file.length() else 0L
|
|
43
|
+
|
|
44
|
+
// Try each strategy's validation
|
|
45
|
+
for (strategy in strategies) {
|
|
46
|
+
if (strategy.isValid(filePath)) {
|
|
47
|
+
CommonHelper.logPrint(CLASS_TAG, "Using strategy for $fileName")
|
|
48
|
+
return strategy.decompress(filePath, destinationPath, progressCallback)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// No valid strategy found - provide detailed error message
|
|
53
|
+
val errorMessage =
|
|
54
|
+
"""
|
|
55
|
+
Failed to decompress file: $fileName ($fileSize bytes)
|
|
56
|
+
|
|
57
|
+
Tried strategies: ZIP (magic bytes 0x504B0304), TAR.GZ (magic bytes 0x1F8B), TAR.BR (file extension)
|
|
58
|
+
|
|
59
|
+
Supported formats:
|
|
60
|
+
- ZIP archives (.zip)
|
|
61
|
+
- GZIP compressed TAR archives (.tar.gz)
|
|
62
|
+
- Brotli compressed TAR archives (.tar.br)
|
|
63
|
+
|
|
64
|
+
Please verify the file is not corrupted and matches one of the supported formats.
|
|
65
|
+
""".trimIndent()
|
|
66
|
+
|
|
67
|
+
Log.e(CLASS_TAG, errorMessage)
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validates if a file is a valid compressed archive.
|
|
73
|
+
* @param filePath Path to the file to validate
|
|
74
|
+
* @return true if the file is a valid compressed archive
|
|
75
|
+
*/
|
|
76
|
+
fun isValidZipFile(filePath: String): Boolean {
|
|
77
|
+
for (strategy in strategies) {
|
|
78
|
+
if (strategy.isValid(filePath)) {
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
CommonHelper.logPrint(CLASS_TAG, "No valid strategy found for file: $filePath")
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package com.instantpaycodepush
|
|
2
|
+
|
|
3
|
+
interface DecompressionStrategy {
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates if a file can be decompressed by this strategy
|
|
7
|
+
* @param filePath Path to the file to validate
|
|
8
|
+
* @return true if the file is valid for this strategy
|
|
9
|
+
*/
|
|
10
|
+
fun isValid(filePath: String): Boolean
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Decompresses a file to the destination directory
|
|
14
|
+
* @param filePath Path to the compressed file
|
|
15
|
+
* @param destinationPath Path to the destination directory
|
|
16
|
+
* @param progressCallback Callback for progress updates (0.0 - 1.0)
|
|
17
|
+
* @return true if decompression was successful, false otherwise
|
|
18
|
+
*/
|
|
19
|
+
fun decompress(
|
|
20
|
+
filePath: String,
|
|
21
|
+
destinationPath: String,
|
|
22
|
+
progressCallback: (Double) -> Unit,
|
|
23
|
+
): Boolean
|
|
24
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
package com.instantpaycodepush
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import java.io.File
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Interface for file system operations
|
|
8
|
+
*/
|
|
9
|
+
interface FileSystemService {
|
|
10
|
+
/**
|
|
11
|
+
* Checks if a file exists at the given path
|
|
12
|
+
*/
|
|
13
|
+
fun fileExists(path: String): Boolean
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates directory at the given path, including any necessary parent directories
|
|
17
|
+
*/
|
|
18
|
+
fun createDirectory(path: String): Boolean
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Removes a file or directory at the given path
|
|
22
|
+
*/
|
|
23
|
+
fun removeItem(path: String): Boolean
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Moves a file or directory from source path to destination path
|
|
27
|
+
*/
|
|
28
|
+
fun moveItem(
|
|
29
|
+
sourcePath: String,
|
|
30
|
+
destinationPath: String,
|
|
31
|
+
): Boolean
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Copies a file or directory from source path to destination path
|
|
35
|
+
*/
|
|
36
|
+
fun copyItem(
|
|
37
|
+
sourcePath: String,
|
|
38
|
+
destinationPath: String,
|
|
39
|
+
): Boolean
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Lists the contents of a directory
|
|
43
|
+
*/
|
|
44
|
+
fun contentsOfDirectory(path: String): List<String>
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Gets the external files directory for the application
|
|
48
|
+
*/
|
|
49
|
+
fun getExternalFilesDir(): File?
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Implementation of FileSystemService using standard File API
|
|
54
|
+
*/
|
|
55
|
+
class FileManagerService(
|
|
56
|
+
private val context: Context,
|
|
57
|
+
) : FileSystemService {
|
|
58
|
+
|
|
59
|
+
override fun fileExists(path: String): Boolean = File(path).exists()
|
|
60
|
+
|
|
61
|
+
override fun createDirectory(path: String): Boolean = File(path).mkdirs()
|
|
62
|
+
|
|
63
|
+
override fun removeItem(path: String): Boolean = File(path).deleteRecursively()
|
|
64
|
+
|
|
65
|
+
override fun moveItem(
|
|
66
|
+
sourcePath: String,
|
|
67
|
+
destinationPath: String,
|
|
68
|
+
): Boolean {
|
|
69
|
+
val source = File(sourcePath)
|
|
70
|
+
val destination = File(destinationPath)
|
|
71
|
+
|
|
72
|
+
return try {
|
|
73
|
+
if (destination.exists()) {
|
|
74
|
+
destination.deleteRecursively()
|
|
75
|
+
}
|
|
76
|
+
source.renameTo(destination)
|
|
77
|
+
} catch (e: Exception) {
|
|
78
|
+
false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
override fun copyItem(
|
|
83
|
+
sourcePath: String,
|
|
84
|
+
destinationPath: String,
|
|
85
|
+
): Boolean {
|
|
86
|
+
val source = File(sourcePath)
|
|
87
|
+
val destination = File(destinationPath)
|
|
88
|
+
|
|
89
|
+
return try {
|
|
90
|
+
if (destination.exists()) {
|
|
91
|
+
destination.deleteRecursively()
|
|
92
|
+
}
|
|
93
|
+
source.copyRecursively(target = destination, overwrite = true)
|
|
94
|
+
} catch (e: Exception) {
|
|
95
|
+
false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
override fun contentsOfDirectory(path: String): List<String> {
|
|
100
|
+
val directory = File(path)
|
|
101
|
+
return directory.listFiles()?.map { it.name } ?: listOf()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override fun getExternalFilesDir(): File? = context.getExternalFilesDir(null)
|
|
105
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package com.instantpaycodepush
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import java.io.File
|
|
5
|
+
import java.security.MessageDigest
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Utility class for file hash operations
|
|
9
|
+
*/
|
|
10
|
+
object HashUtils {
|
|
11
|
+
|
|
12
|
+
private const val CLASS_TAG = "*HashUtils"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Calculates SHA256 hash of a file
|
|
16
|
+
* @param file The file to hash
|
|
17
|
+
* @return Hex string of the hash (lowercase)
|
|
18
|
+
*/
|
|
19
|
+
fun calculateSHA256(file: File): String {
|
|
20
|
+
val digest = MessageDigest.getInstance("SHA-256")
|
|
21
|
+
file.inputStream().use { input ->
|
|
22
|
+
val buffer = ByteArray(8192)
|
|
23
|
+
var bytesRead: Int
|
|
24
|
+
while (input.read(buffer).also { bytesRead = it } != -1) {
|
|
25
|
+
digest.update(buffer, 0, bytesRead)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return digest.digest().joinToString("") { "%02x".format(it) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Verifies file hash
|
|
33
|
+
* @param file File to verify
|
|
34
|
+
* @param expectedHash Expected SHA256 hash (hex string, case-insensitive)
|
|
35
|
+
* @return true if hash matches
|
|
36
|
+
*/
|
|
37
|
+
fun verifyHash(
|
|
38
|
+
file: File,
|
|
39
|
+
expectedHash: String,
|
|
40
|
+
): Boolean {
|
|
41
|
+
val actualHash = calculateSHA256(file)
|
|
42
|
+
val matches = actualHash.equals(expectedHash, ignoreCase = true)
|
|
43
|
+
|
|
44
|
+
if (!matches) {
|
|
45
|
+
CommonHelper.logPrint(CLASS_TAG, "Hash mismatch - Expected: $expectedHash, Actual: $actualHash")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return matches
|
|
49
|
+
}
|
|
50
|
+
}
|