mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
Add setting for requesting user account data.
This commit is contained in:
@@ -492,6 +492,9 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
private void initializeCleanup() {
|
||||
int deleted = SignalDatabase.attachments().deleteAbandonedPreuploadedAttachments();
|
||||
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
|
||||
if (SignalStore.account().clearOldAccountDataReport()) {
|
||||
Log.i(TAG, "Deleted " + deleted + " expired account data report.");
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeGlideCodecs() {
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity
|
||||
import org.thoughtcrime.securesms.lock.v2.KbsConstants
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@@ -121,6 +122,15 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
}
|
||||
)
|
||||
|
||||
if (FeatureFlags.exportAccountData()) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.AccountSettingsFragment__request_account_data),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_exportAccountFragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__delete_account, ContextCompat.getColor(requireContext(), R.color.signal_alert_primary)),
|
||||
onClick = {
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.account.export
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement.Center
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dialogs
|
||||
import org.signal.core.ui.Rows
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
|
||||
class ExportAccountDataFragment : ComposeFragment() {
|
||||
private val viewModel: ExportAccountDataViewModel by viewModels()
|
||||
|
||||
private fun deleteReport() {
|
||||
viewModel.deleteReport()
|
||||
Snackbar.make(requireView(), R.string.ExportAccountDataFragment__delete_report_snackbar, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun exportReport() {
|
||||
val report = viewModel.onGenerateReport()
|
||||
ShareCompat.IntentBuilder(requireContext())
|
||||
.setStream(report.uri)
|
||||
.setType(report.mimeType)
|
||||
.startChooser()
|
||||
}
|
||||
|
||||
private fun dismissExportDialog() {
|
||||
viewModel.dismissExportConfirmationDialog()
|
||||
}
|
||||
|
||||
private fun dismissDeleteDialog() {
|
||||
viewModel.dismissDeleteConfirmationDialog()
|
||||
}
|
||||
|
||||
private fun dismissDownloadErrorDialog() {
|
||||
viewModel.dismissDownloadErrorDialog()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val state: ExportAccountDataState by viewModel.state
|
||||
|
||||
val onNavigationClick: () -> Unit = remember {
|
||||
{ findNavController().popBackStack() }
|
||||
}
|
||||
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(id = R.string.AccountSettingsFragment__request_account_data),
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24),
|
||||
navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close)
|
||||
) { contentPadding ->
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.wrapContentSize()
|
||||
) {
|
||||
LazyColumn(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
|
||||
item {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.export_account_data),
|
||||
contentDescription = stringResource(R.string.ExportAccountDataFragment__your_account_data),
|
||||
modifier = Modifier.padding(top = 47.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.ExportAccountDataFragment__your_account_data),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.ExportAccountDataFragment__export_explanation, stringResource(id = R.string.ExportAccountDataFragment__learn_more)),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(top = 12.dp, start = 32.dp, end = 32.dp, bottom = 20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
if (state.reportDownloaded) {
|
||||
ExportReportOptions(exportAsJson = state.exportAsJson)
|
||||
} else {
|
||||
DownloadReportOptions()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.downloadInProgress) {
|
||||
DownloadProgressDialog()
|
||||
} else if (state.showDownloadFailedDialog) {
|
||||
DownloadFailedDialog()
|
||||
} else if (state.showExportDialog) {
|
||||
ExportReportConfirmationDialog()
|
||||
} else if (state.showDeleteDialog) {
|
||||
DeleteReportConfirmationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadProgressDialog() {
|
||||
Dialog(
|
||||
onDismissRequest = {},
|
||||
DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false)
|
||||
) {
|
||||
Card {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Column(
|
||||
verticalArrangement = Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.padding(top = 50.dp, bottom = 18.dp)
|
||||
.size(42.dp)
|
||||
)
|
||||
Text(text = stringResource(R.string.ExportAccountDataFragment__download_progress), Modifier.padding(bottom = 48.dp, start = 35.dp, end = 35.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadFailedDialog() {
|
||||
Dialogs.SimpleMessageDialog(
|
||||
message = stringResource(id = R.string.ExportAccountDataFragment__report_download_failed),
|
||||
dismiss = stringResource(id = R.string.ExportAccountDataFragment__ok_action),
|
||||
onDismiss = this::dismissDownloadErrorDialog
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeleteReportConfirmationDialog() {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.ExportAccountDataFragment__delete_report_confirmation),
|
||||
body = stringResource(R.string.ExportAccountDataFragment__delete_report_confirmation_message),
|
||||
confirm = stringResource(R.string.ExportAccountDataFragment__delete_report_action),
|
||||
dismiss = stringResource(R.string.ExportAccountDataFragment__cancel_action),
|
||||
onConfirm = this::deleteReport,
|
||||
onDismiss = this::dismissDeleteDialog,
|
||||
confirmColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExportReportConfirmationDialog() {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.ExportAccountDataFragment__export_report_confirmation),
|
||||
body = stringResource(R.string.ExportAccountDataFragment__export_report_confirmation_message),
|
||||
confirm = stringResource(R.string.ExportAccountDataFragment__export_report_action),
|
||||
dismiss = stringResource(R.string.ExportAccountDataFragment__cancel_action),
|
||||
onConfirm = this::exportReport,
|
||||
onDismiss = this::dismissExportDialog
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadReportOptions() {
|
||||
Buttons.LargeTonal(
|
||||
onClick = viewModel::onDownloadReport,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 52.dp, start = 32.dp, end = 32.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.ExportAccountDataFragment__download_report),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExportReportOptions(exportAsJson: Boolean) {
|
||||
Rows.RadioRow(
|
||||
selected = !exportAsJson,
|
||||
text = stringResource(id = R.string.ExportAccountDataFragment__export_as_txt),
|
||||
label = stringResource(id = R.string.ExportAccountDataFragment__export_as_txt_label),
|
||||
modifier = Modifier
|
||||
.clickable(onClick = viewModel::setExportAsTxt)
|
||||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
|
||||
Rows.RadioRow(
|
||||
selected = exportAsJson,
|
||||
text = stringResource(id = R.string.ExportAccountDataFragment__export_as_json),
|
||||
label = stringResource(id = R.string.ExportAccountDataFragment__export_as_json_label),
|
||||
modifier = Modifier
|
||||
.clickable(onClick = viewModel::setExportAsJson)
|
||||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = viewModel::showExportConfirmationDialog,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp, start = 32.dp, end = 32.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.ExportAccountDataFragment__export_report),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = viewModel::showDeleteConfirmationDialog,
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 14.dp, start = 32.dp, end = 32.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.ExportAccountDataFragment__delete_report),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.ExportAccountDataFragment__report_deletion_disclaimer, stringResource(id = R.string.ExportAccountDataFragment__learn_more)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier.padding(top = 16.dp, start = 24.dp, end = 28.dp, bottom = 20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.account.export
|
||||
|
||||
import android.net.Uri
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import java.io.IOException
|
||||
|
||||
class ExportAccountDataRepository(
|
||||
private val accountManager: SignalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager()
|
||||
) {
|
||||
|
||||
fun downloadAccountDataReport(): Completable {
|
||||
return Completable.create {
|
||||
try {
|
||||
SignalStore.account().setAccountDataReport(accountManager.accountDataReport, System.currentTimeMillis())
|
||||
it.onComplete()
|
||||
} catch (e: IOException) {
|
||||
it.onError(e)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun generateAccountDataReport(exportAsJson: Boolean): ExportedReport {
|
||||
val mimeType: String
|
||||
val fileName: String
|
||||
if (exportAsJson) {
|
||||
mimeType = "application/json"
|
||||
fileName = "account-data.json"
|
||||
} else {
|
||||
mimeType = "text/plain"
|
||||
fileName = "account-data.txt"
|
||||
}
|
||||
|
||||
val tree: JsonNode = JsonUtils.getMapper().readTree(SignalStore.account().accountDataReport)
|
||||
val dataStr = if (exportAsJson) {
|
||||
(tree as ObjectNode).remove("text")
|
||||
tree.toString()
|
||||
} else {
|
||||
tree["text"].asText()
|
||||
}
|
||||
|
||||
val uri = BlobProvider.getInstance()
|
||||
.forData(dataStr.encodeToByteArray())
|
||||
.withMimeType(mimeType)
|
||||
.withFileName(fileName)
|
||||
.createForSingleSessionInMemory()
|
||||
|
||||
return ExportedReport(mimeType = mimeType, uri = uri)
|
||||
}
|
||||
|
||||
data class ExportedReport(val mimeType: String, val uri: Uri)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.account.export
|
||||
|
||||
data class ExportAccountDataState(
|
||||
val reportDownloaded: Boolean,
|
||||
val downloadInProgress: Boolean,
|
||||
val exportAsJson: Boolean,
|
||||
val showDownloadFailedDialog: Boolean = false,
|
||||
val showDeleteDialog: Boolean = false,
|
||||
val showExportDialog: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.account.export
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
||||
class ExportAccountDataViewModel(
|
||||
private val repository: ExportAccountDataRepository = ExportAccountDataRepository()
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ExportAccountDataViewModel::class.java)
|
||||
}
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
private val _state = mutableStateOf(
|
||||
ExportAccountDataState(reportDownloaded = false, downloadInProgress = false, exportAsJson = false)
|
||||
)
|
||||
|
||||
val state: State<ExportAccountDataState> = _state
|
||||
|
||||
init {
|
||||
_state.value = _state.value.copy(reportDownloaded = SignalStore.account().hasAccountDataReport())
|
||||
}
|
||||
|
||||
fun onGenerateReport(): ExportAccountDataRepository.ExportedReport = repository.generateAccountDataReport(state.value.exportAsJson)
|
||||
fun onDownloadReport() {
|
||||
_state.value = _state.value.copy(downloadInProgress = true)
|
||||
disposables += repository.downloadAccountDataReport()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({
|
||||
_state.value = _state.value.copy(downloadInProgress = false, reportDownloaded = true)
|
||||
}, { throwable ->
|
||||
Log.e(TAG, throwable)
|
||||
_state.value = _state.value.copy(downloadInProgress = false, showDownloadFailedDialog = true)
|
||||
})
|
||||
}
|
||||
|
||||
fun setExportAsJson() {
|
||||
_state.value = _state.value.copy(exportAsJson = true)
|
||||
}
|
||||
|
||||
fun setExportAsTxt() {
|
||||
_state.value = _state.value.copy(exportAsJson = false)
|
||||
}
|
||||
|
||||
fun showDeleteConfirmationDialog() {
|
||||
_state.value = _state.value.copy(showDeleteDialog = true)
|
||||
}
|
||||
|
||||
fun dismissDeleteConfirmationDialog() {
|
||||
_state.value = _state.value.copy(showDeleteDialog = false)
|
||||
}
|
||||
|
||||
fun dismissDownloadErrorDialog() {
|
||||
_state.value = _state.value.copy(showDownloadFailedDialog = false)
|
||||
}
|
||||
|
||||
fun showExportConfirmationDialog() {
|
||||
_state.value = _state.value.copy(showExportDialog = true)
|
||||
}
|
||||
|
||||
fun dismissExportConfirmationDialog() {
|
||||
_state.value = _state.value.copy(showExportDialog = false)
|
||||
}
|
||||
|
||||
fun deleteReport() {
|
||||
SignalStore.account().deleteAccountDataReport()
|
||||
_state.value = _state.value.copy(reportDownloaded = false)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.dispose()
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,8 @@ import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceIds
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import java.lang.IllegalStateException
|
||||
import java.security.SecureRandom
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
internal class AccountValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
|
||||
@@ -58,6 +58,12 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
|
||||
private const val KEY_PNI_SIGNED_PREKEY_FAILURE_COUNT = "account.pni_signed_prekey_failure_count"
|
||||
private const val KEY_PNI_NEXT_ONE_TIME_PREKEY_ID = "account.pni_next_one_time_prekey_id"
|
||||
|
||||
@VisibleForTesting
|
||||
const val KEY_ACCOUNT_DATA_REPORT = "account.data_report"
|
||||
|
||||
@VisibleForTesting
|
||||
const val KEY_ACCOUNT_DATA_REPORT_DOWNLOAD_TIME = "account.data_report_download_time"
|
||||
|
||||
@VisibleForTesting
|
||||
const val KEY_E164 = "account.e164"
|
||||
|
||||
@@ -318,6 +324,34 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
|
||||
}
|
||||
}
|
||||
|
||||
val accountDataReport: String?
|
||||
get() = getString(KEY_ACCOUNT_DATA_REPORT, null)
|
||||
|
||||
fun setAccountDataReport(report: String, downloadTime: Long) {
|
||||
store.beginWrite()
|
||||
.putString(KEY_ACCOUNT_DATA_REPORT, report)
|
||||
.putLong(KEY_ACCOUNT_DATA_REPORT_DOWNLOAD_TIME, downloadTime)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun hasAccountDataReport(): Boolean = store.containsKey(KEY_ACCOUNT_DATA_REPORT)
|
||||
|
||||
fun clearOldAccountDataReport(): Boolean {
|
||||
return if (hasAccountDataReport() && (getLong(KEY_ACCOUNT_DATA_REPORT_DOWNLOAD_TIME, 0) + 30.days.inWholeMilliseconds) < System.currentTimeMillis()) {
|
||||
deleteAccountDataReport()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAccountDataReport() {
|
||||
store.beginWrite()
|
||||
.remove(KEY_ACCOUNT_DATA_REPORT)
|
||||
.remove(KEY_ACCOUNT_DATA_REPORT_DOWNLOAD_TIME)
|
||||
.apply()
|
||||
}
|
||||
|
||||
val deviceName: String?
|
||||
get() = getString(KEY_DEVICE_NAME, null)
|
||||
|
||||
|
||||
@@ -64,8 +64,6 @@ public final class BlobContentProvider extends BaseContentProvider {
|
||||
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
|
||||
Log.i(TAG, "query() called: " + uri);
|
||||
|
||||
if (projection == null || projection.length <= 0) return null;
|
||||
|
||||
String mimeType = BlobProvider.getMimeType(uri);
|
||||
String fileName = BlobProvider.getFileName(uri);
|
||||
Long fileSize = BlobProvider.getFileSize(uri);
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.annotation.IntRange;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.StreamUtil;
|
||||
@@ -235,6 +236,11 @@ public class BlobProvider {
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public synchronized byte[] getMemoryBlob(@NonNull Uri uri) {
|
||||
return memoryBlobs.get(uri);
|
||||
}
|
||||
|
||||
private static void deleteOrphanedDraftFiles(@NonNull Context context) {
|
||||
File directory = getOrCreateDirectory(context, DRAFT_ATTACHMENTS_DIRECTORY);
|
||||
File[] files = directory.listFiles();
|
||||
|
||||
@@ -108,6 +108,7 @@ public final class FeatureFlags {
|
||||
private static final String ANY_ADDRESS_PORTS_KILL_SWITCH = "android.calling.fieldTrial.anyAddressPortsKillSwitch";
|
||||
private static final String CALLS_TAB = "android.calls.tab";
|
||||
private static final String TEXT_FORMATTING_SPOILER_SEND = "android.textFormatting.spoilerSend";
|
||||
private static final String EXPORT_ACCOUNT_DATA = "android.exportAccountData";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
@@ -166,7 +167,8 @@ public final class FeatureFlags {
|
||||
TEXT_FORMATTING,
|
||||
ANY_ADDRESS_PORTS_KILL_SWITCH,
|
||||
CALLS_TAB,
|
||||
TEXT_FORMATTING_SPOILER_SEND
|
||||
TEXT_FORMATTING_SPOILER_SEND,
|
||||
EXPORT_ACCOUNT_DATA
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -603,6 +605,13 @@ public final class FeatureFlags {
|
||||
return getBoolean(CALLS_TAB, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the ability to export account data is enabled
|
||||
*/
|
||||
public static boolean exportAccountData() {
|
||||
return getBoolean(EXPORT_ACCOUNT_DATA, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
||||
Reference in New Issue
Block a user