Fix avatar loading in OS views when app is not running.

This commit is contained in:
Cody Henthorne
2024-08-19 14:10:20 -04:00
committed by mtang-signal
parent 8a4d9fc635
commit 71b5a9f865
22 changed files with 384 additions and 439 deletions

View File

@@ -153,6 +153,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
DatabaseSecretProvider.getOrCreateDatabaseSecret(this),
AttachmentSecretProvider.getInstance(this).getOrCreateAttachmentSecret());
})
.addBlocking("signal-store", () -> SignalStore.init(this))
.addBlocking("logging", () -> {
initializeLogging();
Log.i(TAG, "onCreate()");

View File

@@ -188,7 +188,7 @@ object BackupRepository {
}
val db = KeyValueDatabase.createWithName(context, "$baseName.db")
SignalStore(KeyValueStore(db))
SignalStore(context, KeyValueStore(db))
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.emoji
import android.net.Uri
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import org.thoughtcrime.securesms.components.emoji.Emoji
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel
@@ -122,7 +123,8 @@ class EmojiSource(
}
}
private fun loadAssetBasedEmojis(): EmojiSource {
@VisibleForTesting
fun loadAssetBasedEmojis(): EmojiSource {
val emojiData: InputStream = AppDependencies.application.assets.open("emoji/emoji_data.json")
emojiData.use {

View File

@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
import androidx.annotation.VisibleForTesting
import org.signal.core.util.Base64
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.IdentityKey
@@ -73,17 +72,10 @@ class AccountValues internal constructor(store: KeyValueStore) : SignalStoreValu
private const val KEY_USERNAME_SYNC_STATE = "phoneNumberPrivacy.usernameSyncState"
private const val KEY_USERNAME_SYNC_ERROR_COUNT = "phoneNumberPrivacy.usernameErrorCount"
@VisibleForTesting
const val KEY_E164 = "account.e164"
@VisibleForTesting
const val KEY_ACI = "account.aci"
@VisibleForTesting
const val KEY_PNI = "account.pni"
@VisibleForTesting
const val KEY_IS_REGISTERED = "account.is_registered"
private const val KEY_E164 = "account.e164"
private const val KEY_ACI = "account.aci"
private const val KEY_PNI = "account.pni"
private const val KEY_IS_REGISTERED = "account.is_registered"
}
init {

View File

@@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.keyvalue
import androidx.annotation.VisibleForTesting
/**
* Values for managing enable/disable state and corresponding alerts for Notification Profiles.
*/
@@ -12,14 +10,9 @@ class NotificationProfileValues(store: KeyValueStore) : SignalStoreValues(store)
private const val KEY_LAST_PROFILE_POPUP_TIME = "np.last_profile_popup_time"
private const val KEY_SEEN_TOOLTIP = "np.seen_tooltip"
@VisibleForTesting
const val KEY_MANUALLY_ENABLED_PROFILE = "np.manually_enabled_profile"
@VisibleForTesting
const val KEY_MANUALLY_ENABLED_UNTIL = "np.manually_enabled_until"
@VisibleForTesting
const val KEY_MANUALLY_DISABLED_AT = "np.manually_disabled_at"
private const val KEY_MANUALLY_ENABLED_PROFILE = "np.manually_enabled_profile"
private const val KEY_MANUALLY_ENABLED_UNTIL = "np.manually_enabled_until"
private const val KEY_MANUALLY_DISABLED_AT = "np.manually_disabled_at"
}
public override fun onFirstEverAppLaunch() = Unit

View File

@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.keyvalue
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@@ -33,6 +32,7 @@ class PaymentsValues internal constructor(store: KeyValueStore) : SignalStoreVal
companion object {
private val TAG = Log.tag(PaymentsValues::class.java)
private const val MOB_PAYMENTS_ENABLED = "mob_payments_enabled"
private const val PAYMENTS_ENTROPY = "payments_entropy"
private const val MOB_LEDGER = "mob_ledger"
private const val PAYMENTS_CURRENT_CURRENCY = "payments_current_currency"
@@ -50,9 +50,6 @@ class PaymentsValues internal constructor(store: KeyValueStore) : SignalStoreVal
private const val SHOW_SAVE_RECOVERY_PHRASE = "mob_show_save_recovery_phrase"
private val LARGE_BALANCE_THRESHOLD = Money.mobileCoin(BigDecimal.valueOf(500))
@VisibleForTesting
const val MOB_PAYMENTS_ENABLED = "mob_payments_enabled"
}
@get:JvmName("isPaymentLockEnabled")

View File

@@ -1,15 +1,14 @@
package org.thoughtcrime.securesms.keyvalue
import android.app.Application
import androidx.annotation.VisibleForTesting
import androidx.preference.PreferenceDataStore
import org.signal.core.util.ResettableLazy
import org.thoughtcrime.securesms.database.KeyValueDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies.application
/**
* Simple, encrypted key-value store.
*/
class SignalStore(private val store: KeyValueStore) {
class SignalStore(context: Application, private val store: KeyValueStore) {
val accountValues = AccountValues(store)
val svrValues = SvrValues(store)
@@ -39,15 +38,22 @@ class SignalStore(private val store: KeyValueStore) {
val apkUpdateValues = ApkUpdateValues(store)
val backupValues = BackupValues(store)
val plainTextValues = PlainTextSharedPrefsDataStore(application)
val plainTextValues = PlainTextSharedPrefsDataStore(context)
companion object {
private var instanceOverride: SignalStore? = null
private val _instance = ResettableLazy {
instanceOverride ?: SignalStore(KeyValueStore(KeyValueDatabase.getInstance(application)))
private var instance: SignalStore? = null
@JvmStatic
fun init(context: Application) {
if (instance == null) {
synchronized(SignalStore::class.java) {
if (instance == null) {
instance = SignalStore(context, KeyValueStore(KeyValueDatabase.getInstance(context)))
}
}
}
}
private val instance by _instance
@JvmStatic
fun onFirstEverAppLaunch() {
@@ -114,7 +120,7 @@ class SignalStore(private val store: KeyValueStore) {
*/
@VisibleForTesting
fun resetCache() {
instance.store.resetCache()
instance!!.store.resetCache()
}
/**
@@ -122,144 +128,144 @@ class SignalStore(private val store: KeyValueStore) {
*/
@JvmStatic
fun onPostBackupRestore() {
instance.store.resetCache()
instance!!.store.resetCache()
}
@JvmStatic
@get:JvmName("account")
val account: AccountValues
get() = instance.accountValues
get() = instance!!.accountValues
@JvmStatic
@get:JvmName("svr")
val svr: SvrValues
get() = instance.svrValues
get() = instance!!.svrValues
@JvmStatic
@get:JvmName("registration")
val registration: RegistrationValues
get() = instance.registrationValues
get() = instance!!.registrationValues
@JvmStatic
@get:JvmName("pin")
val pin: PinValues
get() = instance.pinValues
get() = instance!!.pinValues
val remoteConfig: RemoteConfigValues
get() = instance.remoteConfigValues
get() = instance!!.remoteConfigValues
@JvmStatic
@get:JvmName("storageService")
val storageService: StorageServiceValues
get() = instance.storageServiceValues
get() = instance!!.storageServiceValues
@JvmStatic
@get:JvmName("uiHints")
val uiHints: UiHintValues
get() = instance.uiHintValues
get() = instance!!.uiHintValues
@JvmStatic
@get:JvmName("tooltips")
val tooltips: TooltipValues
get() = instance.tooltipValues
get() = instance!!.tooltipValues
@JvmStatic
@get:JvmName("misc")
val misc: MiscellaneousValues
get() = instance.miscValues
get() = instance!!.miscValues
@JvmStatic
@get:JvmName("internal")
val internal: InternalValues
get() = instance.internalValues
get() = instance!!.internalValues
@JvmStatic
@get:JvmName("emoji")
val emoji: EmojiValues
get() = instance.emojiValues
get() = instance!!.emojiValues
@JvmStatic
@get:JvmName("settings")
val settings: SettingsValues
get() = instance.settingsValues
get() = instance!!.settingsValues
@JvmStatic
@get:JvmName("certificate")
val certificate: CertificateValues
get() = instance.certificateValues
get() = instance!!.certificateValues
@JvmStatic
@get:JvmName("phoneNumberPrivacy")
val phoneNumberPrivacy: PhoneNumberPrivacyValues
get() = instance.phoneNumberPrivacyValues
get() = instance!!.phoneNumberPrivacyValues
@JvmStatic
@get:JvmName("onboarding")
val onboarding: OnboardingValues
get() = instance.onboardingValues
get() = instance!!.onboardingValues
@JvmStatic
@get:JvmName("wallpaper")
val wallpaper: WallpaperValues
get() = instance.wallpaperValues
get() = instance!!.wallpaperValues
@JvmStatic
@get:JvmName("payments")
val payments: PaymentsValues
get() = instance.paymentsValues
get() = instance!!.paymentsValues
@JvmStatic
@get:JvmName("inAppPayments")
val inAppPayments: InAppPaymentValues
get() = instance.inAppPaymentValues
get() = instance!!.inAppPaymentValues
@JvmStatic
@get:JvmName("proxy")
val proxy: ProxyValues
get() = instance.proxyValues
get() = instance!!.proxyValues
@JvmStatic
@get:JvmName("rateLimit")
val rateLimit: RateLimitValues
get() = instance.rateLimitValues
get() = instance!!.rateLimitValues
@JvmStatic
@get:JvmName("chatColors")
val chatColors: ChatColorsValues
get() = instance.chatColorsValues
get() = instance!!.chatColorsValues
val imageEditor: ImageEditorValues
get() = instance.imageEditorValues
get() = instance!!.imageEditorValues
val notificationProfile: NotificationProfileValues
get() = instance.notificationProfileValues
get() = instance!!.notificationProfileValues
@JvmStatic
@get:JvmName("releaseChannel")
val releaseChannel: ReleaseChannelValues
get() = instance.releaseChannelValues
get() = instance!!.releaseChannelValues
@JvmStatic
@get:JvmName("story")
val story: StoryValues
get() = instance.storyValues
get() = instance!!.storyValues
val apkUpdate: ApkUpdateValues
get() = instance.apkUpdateValues
get() = instance!!.apkUpdateValues
@JvmStatic
@get:JvmName("backup")
val backup: BackupValues
get() = instance.backupValues
get() = instance!!.backupValues
val groupsV2AciAuthorizationCache: GroupsV2AuthorizationSignalStoreCache
get() = GroupsV2AuthorizationSignalStoreCache.createAciCache(instance.store)
get() = GroupsV2AuthorizationSignalStoreCache.createAciCache(instance!!.store)
val plaintext: PlainTextSharedPrefsDataStore
get() = instance.plainTextValues
get() = instance!!.plainTextValues
fun getPreferenceDataStore(): PreferenceDataStore {
return SignalPreferenceDataStore(instance.store)
return SignalPreferenceDataStore(instance!!.store)
}
/**
@@ -268,25 +274,7 @@ class SignalStore(private val store: KeyValueStore) {
*/
@JvmStatic
fun blockUntilAllWritesFinished() {
instance.store.blockUntilAllWritesFinished()
}
/**
* Allows you to set a custom KeyValueStore to read from. Only for testing!
*/
@VisibleForTesting
fun testInject(store: KeyValueStore) {
instanceOverride = SignalStore(store)
_instance.reset()
}
/**
* Allows you to set a custom SignalStore to read from. Only for testing!
*/
@VisibleForTesting
fun testInject(store: SignalStore) {
instanceOverride = store
_instance.reset()
instance!!.store.blockUntilAllWritesFinished()
}
}
}

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.providers
import android.app.Application
import android.content.ContentUris
import android.content.ContentValues
import android.content.Intent
@@ -15,10 +16,16 @@ import android.net.Uri
import android.os.ParcelFileDescriptor
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientCreator
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.util.AdaptiveBitmapMetrics
@@ -54,6 +61,21 @@ class AvatarProvider : BaseContentProvider() {
}
}
private fun init(): Application? {
val application = context as? ApplicationContext ?: return null
SqlCipherLibraryLoader.load()
SignalDatabase.init(
application,
DatabaseSecretProvider.getOrCreateDatabaseSecret(application),
AttachmentSecretProvider.getInstance(application).getOrCreateAttachmentSecret()
)
SignalStore.init(application)
return application
}
override fun onCreate(): Boolean {
if (VERBOSE) Log.i(TAG, "onCreate called")
return true
@@ -63,7 +85,9 @@ class AvatarProvider : BaseContentProvider() {
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
if (VERBOSE) Log.i(TAG, "openFile() called!")
if (KeyCachingService.isLocked(context)) {
val application = init() ?: return null
if (KeyCachingService.isLocked(application)) {
Log.w(TAG, "masterSecret was null, abandoning.")
return null
}
@@ -76,7 +100,7 @@ class AvatarProvider : BaseContentProvider() {
if (uriMatcher.match(uri) == AVATAR) {
if (VERBOSE) Log.i(TAG, "Loading avatar.")
try {
val recipient = getRecipientId(uri)?.let { Recipient.resolved(it) } ?: return null
val recipient = getRecipientId(uri)?.let { RecipientCreator.forRecord(application, SignalDatabase.recipients.getRecord(it)) } ?: return null
return getParcelFileDescriptorForAvatar(recipient)
} catch (ioe: IOException) {
Log.w(TAG, ioe)
@@ -91,6 +115,8 @@ class AvatarProvider : BaseContentProvider() {
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
if (VERBOSE) Log.i(TAG, "query() called: $uri")
val application = init() ?: return null
if (SignalDatabase.instance == null) {
Log.w(TAG, "SignalDatabase unavailable")
return null
@@ -99,8 +125,8 @@ class AvatarProvider : BaseContentProvider() {
if (uriMatcher.match(uri) == AVATAR) {
val recipientId = getRecipientId(uri) ?: return null
if (AvatarHelper.hasAvatar(context!!, recipientId)) {
val file: File = AvatarHelper.getAvatarFile(context!!, recipientId)
if (AvatarHelper.hasAvatar(application, recipientId)) {
val file: File = AvatarHelper.getAvatarFile(application, recipientId)
if (file.exists()) {
return createCursor(projection, file.name, file.length())
}
@@ -115,6 +141,8 @@ class AvatarProvider : BaseContentProvider() {
override fun getType(uri: Uri): String? {
if (VERBOSE) Log.i(TAG, "getType() called: $uri")
init() ?: return null
if (SignalDatabase.instance == null) {
Log.w(TAG, "SignalDatabase unavailable")
return null

View File

@@ -9,24 +9,15 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import com.annimon.stream.Stream;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.database.CallLinkTable;
import org.thoughtcrime.securesms.database.DistributionListTables;
import org.thoughtcrime.securesms.database.GroupTable;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListRecord;
import org.thoughtcrime.securesms.database.model.RecipientRecord;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicReference;
@@ -198,62 +189,11 @@ public final class LiveRecipient {
}
private @NonNull Recipient fetchAndCacheRecipientFromDisk(@NonNull RecipientId id) {
RecipientRecord record = recipientTable.getRecord(id);
Recipient recipient;
if (record.getGroupId() != null) {
recipient = getGroupRecipientDetails(record);
} else if (record.getDistributionListId() != null) {
recipient = getDistributionListRecipientDetails(record);
} else if (record.getCallLinkRoomId() != null) {
recipient = getCallLinkRecipientDetails(record);
} else {
recipient = RecipientCreator.forIndividual(context, record);
}
Recipient recipient = RecipientCreator.forRecord(context, recipientTable.getRecord(id));
RecipientIdCache.INSTANCE.put(recipient);
return recipient;
}
@WorkerThread
private @NonNull Recipient getGroupRecipientDetails(@NonNull RecipientRecord record) {
Optional<GroupRecord> groupRecord = groupDatabase.getGroup(record.getId());
if (groupRecord.isPresent()) {
return RecipientCreator.forGroup(groupRecord.get(), record);
} else {
return RecipientCreator.forUnknownGroup(record.getId(), record.getGroupId());
}
}
@WorkerThread
private @NonNull Recipient getDistributionListRecipientDetails(@NonNull RecipientRecord record) {
DistributionListRecord groupRecord = distributionListTables.getList(Objects.requireNonNull(record.getDistributionListId()));
// TODO [stories] We'll have to see what the perf is like for very large distribution lists. We may not be able to support fetching all the members.
if (groupRecord != null) {
String title = groupRecord.isUnknown() ? null : groupRecord.getName();
List<RecipientId> members = Stream.of(groupRecord.getMembers()).filterNot(RecipientId::isUnknown).toList();
return RecipientCreator.forDistributionList(title, members, record);
}
return RecipientCreator.forDistributionList(null, null, record);
}
@WorkerThread
private @NonNull Recipient getCallLinkRecipientDetails(@NonNull RecipientRecord record) {
CallLinkTable.CallLink callLink = SignalDatabase.callLinks().getCallLinkByRoomId(Objects.requireNonNull(record.getCallLinkRoomId()));
if (callLink != null) {
String name = callLink.getState().getName();
return RecipientCreator.forCallLink(name, record, callLink.getAvatarColor());
}
return RecipientCreator.forCallLink(null, record, AvatarColor.UNKNOWN);
}
synchronized void set(@NonNull Recipient recipient) {
this.recipient.set(recipient);
this.liveData.postValue(recipient);

View File

@@ -2,8 +2,10 @@ package org.thoughtcrime.securesms.recipients
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.database.model.RecipientRecord
import org.thoughtcrime.securesms.groups.GroupId
@@ -101,6 +103,22 @@ object RecipientCreator {
)
}
@JvmStatic
@WorkerThread
fun forRecord(context: Context, record: RecipientRecord): Recipient {
val recipient = if (record.groupId != null) {
getGroupRecipientDetails(record)
} else if (record.distributionListId != null) {
getDistributionListRecipientDetails(record)
} else if (record.callLinkRoomId != null) {
getCallLinkRecipientDetails(record)
} else {
forIndividual(context, record)
}
return recipient
}
@JvmStatic
fun forUnknown(): Recipient {
return Recipient.UNKNOWN
@@ -186,4 +204,43 @@ object RecipientCreator {
note = record.note
)
}
@WorkerThread
private fun getGroupRecipientDetails(record: RecipientRecord): Recipient {
val groupRecord = SignalDatabase.groups.getGroup(record.id)
return if (groupRecord.isPresent) {
forGroup(groupRecord.get(), record)
} else {
forUnknownGroup(record.id, record.groupId)
}
}
@WorkerThread
private fun getDistributionListRecipientDetails(record: RecipientRecord): Recipient {
val groupRecord = SignalDatabase.distributionLists.getList(record.distributionListId!!)
// TODO [stories] We'll have to see what the perf is like for very large distribution lists. We may not be able to support fetching all the members.
if (groupRecord != null) {
val title = if (groupRecord.isUnknown) null else groupRecord.name
val members = groupRecord.members.filterNot { it.isUnknown }
return forDistributionList(title, members, record)
}
return forDistributionList(null, null, record)
}
@WorkerThread
private fun getCallLinkRecipientDetails(record: RecipientRecord): Recipient {
val callLink = SignalDatabase.callLinks.getCallLinkByRoomId(record.callLinkRoomId!!)
if (callLink != null) {
val name = callLink.state.name
return forCallLink(name, record, callLink.avatarColor)
}
return forCallLink(null, record, AvatarColor.UNKNOWN)
}
}