react-native-security-suite 0.6.5 → 0.6.6
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/LICENSE +1 -1
- package/README.md +2 -2
- package/android/build.gradle +45 -28
- package/android/src/main/AndroidManifest.xml +47 -0
- package/android/src/main/{AndroidManifestDeprecated.xml → AndroidManifestNew.xml} +1 -2
- package/android/src/main/java/com/securitysuite/NetworkLogger.java +25 -0
- package/android/src/main/java/com/securitysuite/Sslpinning.java +38 -54
- package/android/src/main/java/com/securitysuite/api/BodyDecoder.kt +35 -0
- package/android/src/main/java/com/securitysuite/api/Chucker.kt +108 -0
- package/android/src/main/java/com/securitysuite/api/ChuckerCollector.kt +129 -0
- package/android/src/main/java/com/securitysuite/api/ChuckerInterceptor.kt +280 -0
- package/android/src/main/java/com/securitysuite/api/ExportFormat.kt +12 -0
- package/android/src/main/java/com/securitysuite/api/RetentionManager.kt +109 -0
- package/android/src/main/java/com/securitysuite/internal/data/entity/HttpHeader.kt +8 -0
- package/android/src/main/java/com/securitysuite/internal/data/entity/HttpTransaction.kt +344 -0
- package/android/src/main/java/com/securitysuite/internal/data/entity/HttpTransactionTuple.kt +62 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/Har.kt +13 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/Log.kt +24 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/Browser.kt +11 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/Creator.kt +11 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/Entry.kt +49 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/Page.kt +14 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/Cache.kt +12 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/Header.kt +17 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/Request.kt +35 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/Response.kt +33 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/Timings.kt +25 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/cache/SecondaryRequest.kt +13 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/request/PostData.kt +20 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/request/QueryString.kt +23 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/request/postdata/Params.kt +13 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/response/Content.kt +30 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/page/PageTimings.kt +11 -0
- package/android/src/main/java/com/securitysuite/internal/data/model/DialogData.kt +8 -0
- package/android/src/main/java/com/securitysuite/internal/data/repository/HttpTransactionDatabaseRepository.kt +60 -0
- package/android/src/main/java/com/securitysuite/internal/data/repository/HttpTransactionRepository.kt +33 -0
- package/android/src/main/java/com/securitysuite/internal/data/repository/RepositoryProvider.kt +38 -0
- package/android/src/main/java/com/securitysuite/internal/data/room/ChuckerDatabase.kt +22 -0
- package/android/src/main/java/com/securitysuite/internal/data/room/HttpTransactionDao.kt +53 -0
- package/android/src/main/java/com/securitysuite/internal/support/BitmapUtils.kt +45 -0
- package/android/src/main/java/com/securitysuite/internal/support/CacheDirectoryProvider.kt +11 -0
- package/android/src/main/java/com/securitysuite/internal/support/ChessboardDrawable.kt +76 -0
- package/android/src/main/java/com/securitysuite/internal/support/ChuckerFileProvider.kt +9 -0
- package/android/src/main/java/com/securitysuite/internal/support/ClearDatabaseJobIntentServiceReceiver.kt +14 -0
- package/android/src/main/java/com/securitysuite/internal/support/ClearDatabaseService.kt +32 -0
- package/android/src/main/java/com/securitysuite/internal/support/ContextExt.kt +22 -0
- package/android/src/main/java/com/securitysuite/internal/support/DepletingSource.kt +37 -0
- package/android/src/main/java/com/securitysuite/internal/support/FileFactory.kt +30 -0
- package/android/src/main/java/com/securitysuite/internal/support/FileSaver.kt +41 -0
- package/android/src/main/java/com/securitysuite/internal/support/FormatUtils.kt +117 -0
- package/android/src/main/java/com/securitysuite/internal/support/FormattedUrl.kt +76 -0
- package/android/src/main/java/com/securitysuite/internal/support/HarUtils.kt +28 -0
- package/android/src/main/java/com/securitysuite/internal/support/HttpUrlUtils.kt +11 -0
- package/android/src/main/java/com/securitysuite/internal/support/JsonConverter.kt +19 -0
- package/android/src/main/java/com/securitysuite/internal/support/LimitingSource.kt +22 -0
- package/android/src/main/java/com/securitysuite/internal/support/LiveDataUtils.kt +68 -0
- package/android/src/main/java/com/securitysuite/internal/support/Logger.kt +43 -0
- package/android/src/main/java/com/securitysuite/internal/support/NotificationHelper.kt +149 -0
- package/android/src/main/java/com/securitysuite/internal/support/OkHttpUtils.kt +86 -0
- package/android/src/main/java/com/securitysuite/internal/support/OkioUtils.kt +34 -0
- package/android/src/main/java/com/securitysuite/internal/support/PlainTextDecoder.kt +30 -0
- package/android/src/main/java/com/securitysuite/internal/support/ReportingSink.kt +114 -0
- package/android/src/main/java/com/securitysuite/internal/support/RequestProcessor.kt +102 -0
- package/android/src/main/java/com/securitysuite/internal/support/ResponseProcessor.kt +170 -0
- package/android/src/main/java/com/securitysuite/internal/support/SearchHighlightUtil.kt +80 -0
- package/android/src/main/java/com/securitysuite/internal/support/Sharable.kt +86 -0
- package/android/src/main/java/com/securitysuite/internal/support/SpanTextUtil.kt +202 -0
- package/android/src/main/java/com/securitysuite/internal/support/TeeSource.kt +68 -0
- package/android/src/main/java/com/securitysuite/internal/support/TransactionCurlCommandSharable.kt +46 -0
- package/android/src/main/java/com/securitysuite/internal/support/TransactionDetailsHarSharable.kt +11 -0
- package/android/src/main/java/com/securitysuite/internal/support/TransactionDetailsSharable.kt +73 -0
- package/android/src/main/java/com/securitysuite/internal/support/TransactionDiffCallback.kt +26 -0
- package/android/src/main/java/com/securitysuite/internal/support/TransactionListDetailsSharable.kt +23 -0
- package/android/src/main/java/com/securitysuite/internal/ui/BaseChuckerActivity.kt +35 -0
- package/android/src/main/java/com/securitysuite/internal/ui/MainActivity.kt +375 -0
- package/android/src/main/java/com/securitysuite/internal/ui/MainViewModel.kt +47 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/PayloadType.kt +6 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/ProtocolResources.kt +14 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/TransactionActivity.kt +186 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/TransactionAdapter.kt +139 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/TransactionOverviewFragment.kt +100 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/TransactionPagerAdapter.kt +29 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/TransactionPayloadAdapter.kt +269 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/TransactionPayloadFragment.kt +529 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/TransactionViewModel.kt +69 -0
- package/android/src/main/res/A.java +12 -0
- package/android/src/main/res/color/chucker_fab_background_colour.xml +5 -0
- package/android/src/main/res/drawable/chucker_empty_payload.xml +10 -0
- package/android/src/main/res/drawable/chucker_ic_arrow_down.xml +10 -0
- package/android/src/main/res/drawable/chucker_ic_copy.xml +12 -0
- package/android/src/main/res/drawable/chucker_ic_decoded_url_white.xml +10 -0
- package/android/src/main/res/drawable/chucker_ic_delete_white.xml +9 -0
- package/android/src/main/res/drawable/chucker_ic_encoded_url_white.xml +11 -0
- package/android/src/main/res/drawable/chucker_ic_graphql.xml +27 -0
- package/android/src/main/res/drawable/chucker_ic_http.xml +10 -0
- package/android/src/main/res/drawable/chucker_ic_https.xml +9 -0
- package/android/src/main/res/drawable/chucker_ic_launcher_foreground.xml +14 -0
- package/android/src/main/res/drawable/chucker_ic_save_white.xml +9 -0
- package/android/src/main/res/drawable/chucker_ic_search_white.xml +9 -0
- package/android/src/main/res/drawable/chucker_ic_share_white.xml +9 -0
- package/android/src/main/res/drawable/chucker_ic_transaction_notification.xml +9 -0
- package/android/src/main/res/layout/activity_main.xml +83 -0
- package/android/src/main/res/layout/activity_transaction.xml +48 -0
- package/android/src/main/res/layout/fragment_transaction_overview.xml +365 -0
- package/android/src/main/res/layout/fragment_transaction_payload.xml +132 -0
- package/android/src/main/res/layout/list_item_transaction.xml +122 -0
- package/android/src/main/res/layout/transaction_item_body_line.xml +13 -0
- package/android/src/main/res/layout/transaction_item_copy.xml +19 -0
- package/android/src/main/res/layout/transaction_item_headers.xml +12 -0
- package/android/src/main/res/layout/transaction_item_image.xml +16 -0
- package/android/src/main/res/menu/chucker_transaction.xml +46 -0
- package/android/src/main/res/menu/chucker_transactions_list.xml +41 -0
- package/android/src/main/res/mipmap-anydpi-v26/chucker_ic_launcher.xml +5 -0
- package/android/src/main/res/mipmap-hdpi/chucker_ic_launcher.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/chucker_ic_launcher.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/chucker_ic_launcher.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/chucker_ic_launcher.png +0 -0
- package/android/src/main/res/values/chucker_ic_launcher_background.xml +4 -0
- package/android/src/main/res/values/colors.xml +38 -0
- package/android/src/main/res/values/dimens.xml +10 -0
- package/android/src/main/res/values/public.xml +5 -0
- package/android/src/main/res/values/strings.xml +77 -0
- package/android/src/main/res/values/styles.xml +44 -0
- package/android/src/main/res/values-es/strings.xml +75 -0
- package/android/src/main/res/values-night/colors.xml +32 -0
- package/android/src/main/res/xml/chucker_provider_paths.xml +4 -0
- package/ios/SecuritySuite.swift +0 -2
- package/ios/SslPinning.swift +0 -26
- package/lib/commonjs/helpers.js +1 -1
- package/lib/commonjs/helpers.js.map +1 -1
- package/lib/commonjs/index.js +23 -40
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/helpers.js +3 -1
- package/lib/module/helpers.js.map +1 -1
- package/lib/module/index.js +21 -33
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/helpers.d.ts.map +1 -0
- package/lib/typescript/{index.d.ts → commonjs/src/index.d.ts} +1 -6
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/helpers.d.ts +3 -0
- package/lib/typescript/module/src/helpers.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +72 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/package.json +70 -43
- package/react-native-security-suite.podspec +23 -15
- package/src/helpers.ts +1 -1
- package/src/index.tsx +5 -18
- package/android/src/main/java/com/securitysuite/AndroidLogger.kt +0 -19
- package/android/src/main/java/com/securitysuite/modifier/Base64Decoder.kt +0 -11
- package/android/src/main/java/com/securitysuite/modifier/BasicAuthorizationHeaderModifier.kt +0 -16
- package/ios/SecuritySuite.xcodeproj/project.pbxproj +0 -293
- package/ios/SecuritySuite.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/ios/SecuritySuite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/ios/SecuritySuite.xcodeproj/project.xcworkspace/xcuserdata/m.navabifar.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/SecuritySuite.xcodeproj/xcuserdata/m.navabifar.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/lib/typescript/helpers.d.ts.map +0 -1
- package/lib/typescript/index.d.ts.map +0 -1
- /package/lib/typescript/{helpers.d.ts → commonjs/src/helpers.d.ts} +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package com.securitysuite.internal.data.har.log.entry.cache
|
|
2
|
+
|
|
3
|
+
import com.google.gson.annotations.SerializedName
|
|
4
|
+
|
|
5
|
+
// https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#beforerequest--afterrequest
|
|
6
|
+
// http://www.softwareishard.com/blog/har-12-spec/#cache
|
|
7
|
+
internal data class SecondaryRequest(
|
|
8
|
+
@SerializedName("expires") val expires: String? = null,
|
|
9
|
+
@SerializedName("lastAccess") val lastAccess: String,
|
|
10
|
+
@SerializedName("eTag") val eTag: String,
|
|
11
|
+
@SerializedName("hitCount") val hitCount: Int,
|
|
12
|
+
@SerializedName("comment") val comment: String? = null,
|
|
13
|
+
)
|
package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/request/PostData.kt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package com.securitysuite.internal.data.har.log.entry.request
|
|
2
|
+
|
|
3
|
+
import com.securitysuite.internal.data.entity.HttpTransaction
|
|
4
|
+
import com.securitysuite.internal.data.har.log.entry.request.postdata.Params
|
|
5
|
+
import com.google.gson.annotations.SerializedName
|
|
6
|
+
|
|
7
|
+
// https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#postdata
|
|
8
|
+
// http://www.softwareishard.com/blog/har-12-spec/#postData
|
|
9
|
+
// text and params fields are mutually exclusive.
|
|
10
|
+
internal data class PostData(
|
|
11
|
+
@SerializedName("mimeType") val mimeType: String,
|
|
12
|
+
@SerializedName("params") val params: Params? = null,
|
|
13
|
+
@SerializedName("text") val text: String? = null,
|
|
14
|
+
@SerializedName("comment") val comment: String? = null,
|
|
15
|
+
) {
|
|
16
|
+
constructor(transaction: HttpTransaction) : this(
|
|
17
|
+
mimeType = transaction.requestContentType ?: "application/octet-stream",
|
|
18
|
+
text = transaction.requestBody,
|
|
19
|
+
)
|
|
20
|
+
}
|
package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/request/QueryString.kt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package com.securitysuite.internal.data.har.log.entry.request
|
|
2
|
+
|
|
3
|
+
import com.google.gson.annotations.SerializedName
|
|
4
|
+
import okhttp3.HttpUrl
|
|
5
|
+
|
|
6
|
+
// https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#querystring
|
|
7
|
+
// http://www.softwareishard.com/blog/har-12-spec/#queryString
|
|
8
|
+
internal data class QueryString(
|
|
9
|
+
@SerializedName("name") val name: String,
|
|
10
|
+
@SerializedName("value") val value: String,
|
|
11
|
+
@SerializedName("comment") val comment: String? = null,
|
|
12
|
+
) {
|
|
13
|
+
companion object {
|
|
14
|
+
fun fromUrl(url: HttpUrl): List<QueryString> {
|
|
15
|
+
return List(url.querySize) { index ->
|
|
16
|
+
QueryString(
|
|
17
|
+
name = url.queryParameterName(index),
|
|
18
|
+
value = url.queryParameterValue(index).orEmpty(),
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package com.securitysuite.internal.data.har.log.entry.request.postdata
|
|
2
|
+
|
|
3
|
+
import com.google.gson.annotations.SerializedName
|
|
4
|
+
|
|
5
|
+
// https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#params
|
|
6
|
+
// http://www.softwareishard.com/blog/har-12-spec/#params
|
|
7
|
+
internal data class Params(
|
|
8
|
+
@SerializedName("name") val name: String,
|
|
9
|
+
@SerializedName("value") val value: String? = null,
|
|
10
|
+
@SerializedName("fileName") val fileName: String? = null,
|
|
11
|
+
@SerializedName("contentType") val contentType: String? = null,
|
|
12
|
+
@SerializedName("comment") val comment: String? = null,
|
|
13
|
+
)
|
package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/response/Content.kt
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.securitysuite.internal.data.har.log.entry.response
|
|
2
|
+
|
|
3
|
+
import com.securitysuite.internal.data.entity.HttpTransaction
|
|
4
|
+
import com.google.gson.annotations.SerializedName
|
|
5
|
+
|
|
6
|
+
// https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#content
|
|
7
|
+
// http://www.softwareishard.com/blog/har-12-spec/#content
|
|
8
|
+
internal data class Content(
|
|
9
|
+
@SerializedName("size") val size: Long? = null,
|
|
10
|
+
@SerializedName("compression") val compression: Int? = null,
|
|
11
|
+
@SerializedName("mimeType") val mimeType: String,
|
|
12
|
+
@SerializedName("text") val text: String? = null,
|
|
13
|
+
@SerializedName("encoding") val encoding: String? = null,
|
|
14
|
+
@SerializedName("comment") val comment: String? = null,
|
|
15
|
+
) {
|
|
16
|
+
companion object {
|
|
17
|
+
internal val EMPTY =
|
|
18
|
+
Content(
|
|
19
|
+
size = 0L,
|
|
20
|
+
compression = 0,
|
|
21
|
+
mimeType = "text/plain",
|
|
22
|
+
text = "",
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
constructor(transaction: HttpTransaction) : this(
|
|
26
|
+
size = transaction.responsePayloadSize,
|
|
27
|
+
mimeType = transaction.responseContentType ?: "application/octet-stream",
|
|
28
|
+
text = transaction.responseBody,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
package com.securitysuite.internal.data.har.log.page
|
|
2
|
+
|
|
3
|
+
import com.google.gson.annotations.SerializedName
|
|
4
|
+
|
|
5
|
+
// https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md#pagetimings
|
|
6
|
+
// http://www.softwareishard.com/blog/har-12-spec/#pageTimings
|
|
7
|
+
internal data class PageTimings(
|
|
8
|
+
@SerializedName("onContentLoad") val onContentLoad: Long? = null,
|
|
9
|
+
@SerializedName("onLoad") val onLoad: Long? = null,
|
|
10
|
+
@SerializedName("comment") val comment: String? = null,
|
|
11
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package com.securitysuite.internal.data.repository
|
|
2
|
+
|
|
3
|
+
import androidx.lifecycle.LiveData
|
|
4
|
+
import com.securitysuite.internal.data.entity.HttpTransaction
|
|
5
|
+
import com.securitysuite.internal.data.entity.HttpTransactionTuple
|
|
6
|
+
import com.securitysuite.internal.data.room.ChuckerDatabase
|
|
7
|
+
import com.securitysuite.internal.support.distinctUntilChanged
|
|
8
|
+
|
|
9
|
+
internal class HttpTransactionDatabaseRepository(private val database: ChuckerDatabase) : HttpTransactionRepository {
|
|
10
|
+
private val transactionDao get() = database.transactionDao()
|
|
11
|
+
|
|
12
|
+
override fun getFilteredTransactionTuples(
|
|
13
|
+
code: String,
|
|
14
|
+
path: String,
|
|
15
|
+
): LiveData<List<HttpTransactionTuple>> {
|
|
16
|
+
val pathQuery = if (path.isNotEmpty()) "%$path%" else "%"
|
|
17
|
+
return transactionDao.getFilteredTuples(
|
|
18
|
+
"$code%",
|
|
19
|
+
pathQuery = pathQuery,
|
|
20
|
+
/*
|
|
21
|
+
* Refer <a href='https://github.com/ChuckerTeam/chucker/issues/847">Issue #847</a> for
|
|
22
|
+
* more context
|
|
23
|
+
*/
|
|
24
|
+
graphQlQuery = pathQuery,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun getTransaction(transactionId: Long): LiveData<HttpTransaction?> {
|
|
29
|
+
return transactionDao.getById(transactionId)
|
|
30
|
+
.distinctUntilChanged { old, new -> old?.hasTheSameContent(new) != false }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override fun getSortedTransactionTuples(): LiveData<List<HttpTransactionTuple>> {
|
|
34
|
+
return transactionDao.getSortedTuples()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override suspend fun deleteAllTransactions() {
|
|
38
|
+
transactionDao.deleteAll()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override suspend fun insertTransaction(transaction: HttpTransaction) {
|
|
42
|
+
val id = transactionDao.insert(transaction)
|
|
43
|
+
transaction.id = id ?: 0
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
override suspend fun updateTransaction(transaction: HttpTransaction): Int {
|
|
47
|
+
return transactionDao.update(transaction)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override suspend fun deleteOldTransactions(threshold: Long) {
|
|
51
|
+
transactionDao.deleteBefore(threshold)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override suspend fun getAllTransactions(): List<HttpTransaction> = transactionDao.getAll()
|
|
55
|
+
|
|
56
|
+
override fun getTransactionsInTimeRange(minTimestamp: Long?): List<HttpTransaction> {
|
|
57
|
+
val timestamp = minTimestamp ?: 0L
|
|
58
|
+
return transactionDao.getTransactionsInTimeRange(timestamp)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.securitysuite.internal.data.repository
|
|
2
|
+
|
|
3
|
+
import androidx.lifecycle.LiveData
|
|
4
|
+
import com.securitysuite.internal.data.entity.HttpTransaction
|
|
5
|
+
import com.securitysuite.internal.data.entity.HttpTransactionTuple
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Repository Interface representing all the operations that are needed to let Chucker work
|
|
9
|
+
* with [HttpTransaction] and [HttpTransactionTuple]. Please use [HttpTransactionDatabaseRepository] that
|
|
10
|
+
* uses Room and SqLite to run those operations.
|
|
11
|
+
*/
|
|
12
|
+
internal interface HttpTransactionRepository {
|
|
13
|
+
suspend fun insertTransaction(transaction: HttpTransaction)
|
|
14
|
+
|
|
15
|
+
suspend fun updateTransaction(transaction: HttpTransaction): Int
|
|
16
|
+
|
|
17
|
+
suspend fun deleteOldTransactions(threshold: Long)
|
|
18
|
+
|
|
19
|
+
suspend fun deleteAllTransactions()
|
|
20
|
+
|
|
21
|
+
fun getSortedTransactionTuples(): LiveData<List<HttpTransactionTuple>>
|
|
22
|
+
|
|
23
|
+
fun getFilteredTransactionTuples(
|
|
24
|
+
code: String,
|
|
25
|
+
path: String,
|
|
26
|
+
): LiveData<List<HttpTransactionTuple>>
|
|
27
|
+
|
|
28
|
+
fun getTransaction(transactionId: Long): LiveData<HttpTransaction?>
|
|
29
|
+
|
|
30
|
+
suspend fun getAllTransactions(): List<HttpTransaction>
|
|
31
|
+
|
|
32
|
+
fun getTransactionsInTimeRange(minTimestamp: Long?): List<HttpTransaction>
|
|
33
|
+
}
|
package/android/src/main/java/com/securitysuite/internal/data/repository/RepositoryProvider.kt
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.securitysuite.internal.data.repository
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.annotation.VisibleForTesting
|
|
5
|
+
import com.securitysuite.internal.data.repository.RepositoryProvider.initialize
|
|
6
|
+
import com.securitysuite.internal.data.room.ChuckerDatabase
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A singleton to hold the [HttpTransactionRepository] instance.
|
|
10
|
+
* Make sure you call [initialize] before accessing the stored instance.
|
|
11
|
+
*/
|
|
12
|
+
internal object RepositoryProvider {
|
|
13
|
+
private var transactionRepository: HttpTransactionRepository? = null
|
|
14
|
+
|
|
15
|
+
fun transaction(): HttpTransactionRepository {
|
|
16
|
+
return checkNotNull(transactionRepository) {
|
|
17
|
+
"You can't access the transaction repository if you don't initialize it!"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Idempotent method. Must be called before accessing the repositories.
|
|
23
|
+
*/
|
|
24
|
+
fun initialize(applicationContext: Context) {
|
|
25
|
+
if (transactionRepository == null) {
|
|
26
|
+
val db = ChuckerDatabase.create(applicationContext)
|
|
27
|
+
transactionRepository = HttpTransactionDatabaseRepository(db)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Cleanup stored singleton objects
|
|
33
|
+
*/
|
|
34
|
+
@VisibleForTesting
|
|
35
|
+
fun close() {
|
|
36
|
+
transactionRepository = null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.securitysuite.internal.data.room
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.room.Database
|
|
5
|
+
import androidx.room.Room
|
|
6
|
+
import androidx.room.RoomDatabase
|
|
7
|
+
import com.securitysuite.internal.data.entity.HttpTransaction
|
|
8
|
+
|
|
9
|
+
@Database(entities = [HttpTransaction::class], version = 9, exportSchema = false)
|
|
10
|
+
internal abstract class ChuckerDatabase : RoomDatabase() {
|
|
11
|
+
abstract fun transactionDao(): HttpTransactionDao
|
|
12
|
+
|
|
13
|
+
companion object {
|
|
14
|
+
private const val DB_NAME = "chucker.db"
|
|
15
|
+
|
|
16
|
+
fun create(applicationContext: Context): ChuckerDatabase {
|
|
17
|
+
return Room.databaseBuilder(applicationContext, ChuckerDatabase::class.java, DB_NAME)
|
|
18
|
+
.fallbackToDestructiveMigration()
|
|
19
|
+
.build()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package com.securitysuite.internal.data.room
|
|
2
|
+
|
|
3
|
+
import androidx.lifecycle.LiveData
|
|
4
|
+
import androidx.room.Dao
|
|
5
|
+
import androidx.room.Insert
|
|
6
|
+
import androidx.room.OnConflictStrategy
|
|
7
|
+
import androidx.room.Query
|
|
8
|
+
import androidx.room.Update
|
|
9
|
+
import com.securitysuite.internal.data.entity.HttpTransaction
|
|
10
|
+
import com.securitysuite.internal.data.entity.HttpTransactionTuple
|
|
11
|
+
|
|
12
|
+
@Dao
|
|
13
|
+
internal interface HttpTransactionDao {
|
|
14
|
+
@Query(
|
|
15
|
+
"SELECT id, requestDate, tookMs, protocol, method, host, path, scheme, responseCode, " +
|
|
16
|
+
"requestPayloadSize, responsePayloadSize, error, graphQLDetected, graphQlOperationName FROM " +
|
|
17
|
+
"transactions ORDER BY requestDate DESC",
|
|
18
|
+
)
|
|
19
|
+
fun getSortedTuples(): LiveData<List<HttpTransactionTuple>>
|
|
20
|
+
|
|
21
|
+
@Query(
|
|
22
|
+
"SELECT id, requestDate, tookMs, protocol, method, host, path, scheme, responseCode, " +
|
|
23
|
+
"requestPayloadSize, responsePayloadSize, error, graphQLDetected, graphQlOperationName FROM " +
|
|
24
|
+
"transactions WHERE responseCode LIKE :codeQuery AND (path LIKE :pathQuery OR " +
|
|
25
|
+
"graphQlOperationName LIKE :graphQlQuery) ORDER BY requestDate DESC",
|
|
26
|
+
)
|
|
27
|
+
fun getFilteredTuples(
|
|
28
|
+
codeQuery: String,
|
|
29
|
+
pathQuery: String,
|
|
30
|
+
graphQlQuery: String = "",
|
|
31
|
+
): LiveData<List<HttpTransactionTuple>>
|
|
32
|
+
|
|
33
|
+
@Insert
|
|
34
|
+
suspend fun insert(transaction: HttpTransaction): Long?
|
|
35
|
+
|
|
36
|
+
@Update(onConflict = OnConflictStrategy.REPLACE)
|
|
37
|
+
suspend fun update(transaction: HttpTransaction): Int
|
|
38
|
+
|
|
39
|
+
@Query("DELETE FROM transactions")
|
|
40
|
+
suspend fun deleteAll(): Int
|
|
41
|
+
|
|
42
|
+
@Query("SELECT * FROM transactions WHERE id = :id")
|
|
43
|
+
fun getById(id: Long): LiveData<HttpTransaction?>
|
|
44
|
+
|
|
45
|
+
@Query("DELETE FROM transactions WHERE requestDate <= :threshold")
|
|
46
|
+
suspend fun deleteBefore(threshold: Long): Int
|
|
47
|
+
|
|
48
|
+
@Query("SELECT * FROM transactions")
|
|
49
|
+
suspend fun getAll(): List<HttpTransaction>
|
|
50
|
+
|
|
51
|
+
@Query("SELECT * FROM transactions WHERE requestDate >= :timestamp")
|
|
52
|
+
fun getTransactionsInTimeRange(timestamp: Long): List<HttpTransaction>
|
|
53
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.graphics.Matrix
|
|
7
|
+
import android.graphics.Paint
|
|
8
|
+
import androidx.annotation.ColorInt
|
|
9
|
+
import androidx.core.graphics.ColorUtils
|
|
10
|
+
import androidx.palette.graphics.Palette
|
|
11
|
+
import kotlinx.coroutines.Dispatchers
|
|
12
|
+
import kotlinx.coroutines.withContext
|
|
13
|
+
|
|
14
|
+
private val BITMAP_PAINT = Paint(Paint.FILTER_BITMAP_FLAG)
|
|
15
|
+
|
|
16
|
+
internal suspend fun Bitmap.calculateLuminance(): Double? {
|
|
17
|
+
val color = Color.MAGENTA
|
|
18
|
+
return withContext(Dispatchers.Default) {
|
|
19
|
+
val alpha = replaceAlphaWithColor(color)
|
|
20
|
+
return@withContext alpha.getLuminance(color)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private fun Bitmap.replaceAlphaWithColor(
|
|
25
|
+
@ColorInt color: Int,
|
|
26
|
+
): Bitmap {
|
|
27
|
+
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
28
|
+
result.eraseColor(color)
|
|
29
|
+
Canvas(result).apply {
|
|
30
|
+
drawBitmap(this@replaceAlphaWithColor, Matrix(), BITMAP_PAINT)
|
|
31
|
+
}
|
|
32
|
+
return result
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private fun Bitmap.getLuminance(
|
|
36
|
+
@ColorInt alphaSubstitute: Int,
|
|
37
|
+
): Double? {
|
|
38
|
+
val imagePalette =
|
|
39
|
+
Palette.from(this)
|
|
40
|
+
.clearFilters()
|
|
41
|
+
.addFilter { rgb, _ -> (rgb != alphaSubstitute) }
|
|
42
|
+
.generate()
|
|
43
|
+
val dominantSwatch = imagePalette.dominantSwatch
|
|
44
|
+
return dominantSwatch?.rgb?.let(ColorUtils::calculateLuminance)
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An interface that returns a reference to a cache directory where temporary files can be
|
|
7
|
+
* saved.
|
|
8
|
+
*/
|
|
9
|
+
internal fun interface CacheDirectoryProvider {
|
|
10
|
+
fun provide(): File?
|
|
11
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.Bitmap.Config.ARGB_8888
|
|
6
|
+
import android.graphics.BitmapShader
|
|
7
|
+
import android.graphics.Canvas
|
|
8
|
+
import android.graphics.ColorFilter
|
|
9
|
+
import android.graphics.Paint
|
|
10
|
+
import android.graphics.Paint.Style.FILL
|
|
11
|
+
import android.graphics.PixelFormat
|
|
12
|
+
import android.graphics.Rect
|
|
13
|
+
import android.graphics.Shader.TileMode.REPEAT
|
|
14
|
+
import android.graphics.drawable.Drawable
|
|
15
|
+
import androidx.annotation.ColorInt
|
|
16
|
+
import androidx.annotation.ColorRes
|
|
17
|
+
import androidx.annotation.DimenRes
|
|
18
|
+
import androidx.annotation.Px
|
|
19
|
+
import androidx.core.content.ContextCompat
|
|
20
|
+
|
|
21
|
+
internal class ChessboardDrawable(
|
|
22
|
+
@ColorInt evenColor: Int,
|
|
23
|
+
@ColorInt oddColor: Int,
|
|
24
|
+
@Px squareSize: Int,
|
|
25
|
+
) : Drawable() {
|
|
26
|
+
private val chessboardPaint =
|
|
27
|
+
Paint().apply {
|
|
28
|
+
val patternBitmap = Bitmap.createBitmap(squareSize * 2, squareSize * 2, ARGB_8888)
|
|
29
|
+
patternBitmap.eraseColor(evenColor)
|
|
30
|
+
|
|
31
|
+
color = oddColor
|
|
32
|
+
style = FILL
|
|
33
|
+
val patternCanvas = Canvas(patternBitmap)
|
|
34
|
+
val squareRect = Rect(squareSize, 0, 2 * squareSize, squareSize)
|
|
35
|
+
patternCanvas.drawRect(squareRect, this)
|
|
36
|
+
squareRect.offsetTo(0, squareSize)
|
|
37
|
+
patternCanvas.drawRect(squareRect, this)
|
|
38
|
+
|
|
39
|
+
reset()
|
|
40
|
+
shader = BitmapShader(patternBitmap, REPEAT, REPEAT)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override fun draw(canvas: Canvas) {
|
|
44
|
+
canvas.drawPaint(chessboardPaint)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override fun setAlpha(alpha: Int) {
|
|
48
|
+
chessboardPaint.alpha = alpha
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override fun getOpacity(): Int {
|
|
52
|
+
return if (chessboardPaint.colorFilter == null) {
|
|
53
|
+
PixelFormat.OPAQUE
|
|
54
|
+
} else {
|
|
55
|
+
PixelFormat.TRANSLUCENT
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override fun setColorFilter(colorFilter: ColorFilter?) {
|
|
60
|
+
chessboardPaint.colorFilter = colorFilter
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
companion object {
|
|
64
|
+
fun createPattern(
|
|
65
|
+
context: Context,
|
|
66
|
+
@ColorRes evenColorId: Int,
|
|
67
|
+
@ColorRes oddColorId: Int,
|
|
68
|
+
@DimenRes sizeId: Int,
|
|
69
|
+
): ChessboardDrawable {
|
|
70
|
+
val evenColor = ContextCompat.getColor(context, evenColorId)
|
|
71
|
+
val oddColor = ContextCompat.getColor(context, oddColorId)
|
|
72
|
+
val size = context.resources.getDimensionPixelSize(sizeId)
|
|
73
|
+
return ChessboardDrawable(evenColor, oddColor, size)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import androidx.core.content.FileProvider
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* We need our own subclass so we don't conflict with other [FileProvider]s
|
|
7
|
+
* See: https://github.com/ChuckerTeam/chucker/issues/409
|
|
8
|
+
*/
|
|
9
|
+
internal class ChuckerFileProvider : FileProvider()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
|
|
7
|
+
internal class ClearDatabaseJobIntentServiceReceiver : BroadcastReceiver() {
|
|
8
|
+
override fun onReceive(
|
|
9
|
+
context: Context,
|
|
10
|
+
intent: Intent,
|
|
11
|
+
) {
|
|
12
|
+
ClearDatabaseService.enqueueWork(context, intent)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import androidx.core.app.JobIntentService
|
|
6
|
+
import com.securitysuite.internal.data.repository.RepositoryProvider
|
|
7
|
+
import kotlinx.coroutines.MainScope
|
|
8
|
+
import kotlinx.coroutines.launch
|
|
9
|
+
|
|
10
|
+
internal class ClearDatabaseService : JobIntentService() {
|
|
11
|
+
private val scope = MainScope()
|
|
12
|
+
|
|
13
|
+
override fun onHandleWork(intent: Intent) {
|
|
14
|
+
RepositoryProvider.initialize(applicationContext)
|
|
15
|
+
scope.launch {
|
|
16
|
+
RepositoryProvider.transaction().deleteAllTransactions()
|
|
17
|
+
NotificationHelper.clearBuffer()
|
|
18
|
+
NotificationHelper(applicationContext).dismissNotifications()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
companion object {
|
|
23
|
+
private const val CLEAN_DATABASE_JOB_ID = 123321
|
|
24
|
+
|
|
25
|
+
fun enqueueWork(
|
|
26
|
+
context: Context,
|
|
27
|
+
work: Intent,
|
|
28
|
+
) {
|
|
29
|
+
enqueueWork(context, ClearDatabaseService::class.java, CLEAN_DATABASE_JOB_ID, work)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.securitysuite.internal.data.model.DialogData
|
|
5
|
+
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
6
|
+
|
|
7
|
+
internal fun Context.showDialog(
|
|
8
|
+
dialogData: DialogData,
|
|
9
|
+
onPositiveClick: (() -> Unit)?,
|
|
10
|
+
onNegativeClick: (() -> Unit)?,
|
|
11
|
+
) {
|
|
12
|
+
MaterialAlertDialogBuilder(this)
|
|
13
|
+
.setTitle(dialogData.title)
|
|
14
|
+
.setMessage(dialogData.message)
|
|
15
|
+
.setPositiveButton(dialogData.positiveButtonText) { _, _ ->
|
|
16
|
+
onPositiveClick?.invoke()
|
|
17
|
+
}
|
|
18
|
+
.setNegativeButton(dialogData.negativeButtonText) { _, _ ->
|
|
19
|
+
onNegativeClick?.invoke()
|
|
20
|
+
}
|
|
21
|
+
.show()
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import okio.Buffer
|
|
4
|
+
import okio.ForwardingSource
|
|
5
|
+
import okio.Source
|
|
6
|
+
import okio.blackholeSink
|
|
7
|
+
import okio.buffer
|
|
8
|
+
import java.io.IOException
|
|
9
|
+
|
|
10
|
+
internal class DepletingSource(delegate: Source) : ForwardingSource(delegate) {
|
|
11
|
+
private var shouldDeplete = true
|
|
12
|
+
|
|
13
|
+
override fun read(
|
|
14
|
+
sink: Buffer,
|
|
15
|
+
byteCount: Long,
|
|
16
|
+
) = try {
|
|
17
|
+
val bytesRead = super.read(sink, byteCount)
|
|
18
|
+
if (bytesRead == -1L) shouldDeplete = false
|
|
19
|
+
bytesRead
|
|
20
|
+
} catch (e: IOException) {
|
|
21
|
+
shouldDeplete = false
|
|
22
|
+
throw e
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override fun close() {
|
|
26
|
+
if (shouldDeplete) {
|
|
27
|
+
try {
|
|
28
|
+
delegate.buffer().readAll(blackholeSink())
|
|
29
|
+
} catch (e: IOException) {
|
|
30
|
+
Logger.error("An error occurred while depleting the source", e)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
shouldDeplete = false
|
|
34
|
+
|
|
35
|
+
super.close()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
import java.io.IOException
|
|
5
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
6
|
+
|
|
7
|
+
internal object FileFactory {
|
|
8
|
+
private val uniqueIdGenerator = AtomicLong()
|
|
9
|
+
|
|
10
|
+
fun create(directory: File) = create(directory, fileName = "chucker-${uniqueIdGenerator.getAndIncrement()}")
|
|
11
|
+
|
|
12
|
+
fun create(
|
|
13
|
+
directory: File,
|
|
14
|
+
fileName: String,
|
|
15
|
+
): File? =
|
|
16
|
+
try {
|
|
17
|
+
File(directory, fileName).apply {
|
|
18
|
+
if (exists() && !delete()) {
|
|
19
|
+
throw IOException("Failed to delete file $this")
|
|
20
|
+
}
|
|
21
|
+
parentFile?.mkdirs()
|
|
22
|
+
if (!createNewFile()) {
|
|
23
|
+
throw IOException("File $this already exists")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch (e: IOException) {
|
|
27
|
+
Logger.error("An error occurred while creating a file", e)
|
|
28
|
+
null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import android.content.ContentResolver
|
|
4
|
+
import android.net.Uri
|
|
5
|
+
import kotlinx.coroutines.Dispatchers
|
|
6
|
+
import kotlinx.coroutines.withContext
|
|
7
|
+
import okio.Source
|
|
8
|
+
import okio.buffer
|
|
9
|
+
import okio.sink
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Utility class to save a file from a [Source] to a [Uri].
|
|
13
|
+
*/
|
|
14
|
+
public object FileSaver {
|
|
15
|
+
/**
|
|
16
|
+
* Saves the data from the [source] to the file at the [uri] using the [contentResolver].
|
|
17
|
+
*
|
|
18
|
+
* @param source The source of the data to save.
|
|
19
|
+
* @param uri The URI of the file to save the data to.
|
|
20
|
+
* @param contentResolver The content resolver to use to save the data.
|
|
21
|
+
* @return `true` if the data was saved successfully, `false` otherwise.
|
|
22
|
+
*/
|
|
23
|
+
public suspend fun saveFile(
|
|
24
|
+
source: Source,
|
|
25
|
+
uri: Uri,
|
|
26
|
+
contentResolver: ContentResolver,
|
|
27
|
+
): Boolean =
|
|
28
|
+
withContext(Dispatchers.IO) {
|
|
29
|
+
runCatching {
|
|
30
|
+
contentResolver.openOutputStream(uri)?.use { outputStream ->
|
|
31
|
+
outputStream.sink().buffer().use { sink ->
|
|
32
|
+
sink.writeAll(source)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}.onFailure {
|
|
36
|
+
Logger.error("Failed to save data to a file", it)
|
|
37
|
+
return@withContext false
|
|
38
|
+
}
|
|
39
|
+
return@withContext true
|
|
40
|
+
}
|
|
41
|
+
}
|