Fix talkback saying meters instead of minutes.

This commit is contained in:
Michelle Tang
2025-12-17 13:20:36 -05:00
committed by jeffrey-signal
parent 942c155723
commit 282a707bf9
20 changed files with 106 additions and 36 deletions

View File

@@ -346,6 +346,7 @@ class CallLogAdapter(
private fun presentCallInfo(call: CallLogRow.Call, date: Long) {
val callState = context.getString(getCallStateStringRes(call.record, call.children.size))
val (dateString, dateContentDescription) = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), date)
binding.callInfo.text = context.getString(
R.string.CallLogAdapter__s_dot_s,
if (call.children.size > 1) {
@@ -353,7 +354,16 @@ class CallLogAdapter(
} else {
callState
},
DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), date)
dateString
)
binding.callInfo.contentDescription = context.getString(
R.string.CallLogAdapter__s_dot_s,
if (call.children.size > 1) {
context.getString(R.string.CallLogAdapter__d_s, call.children.size, callState)
} else {
callState
},
dateContentDescription
)
binding.callInfo.setRelativeDrawables(

View File

@@ -306,16 +306,21 @@ public class ConversationItemFooter extends ConstraintLayout {
long timestamp = (displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) ? messageRecord.getDateSent() : messageRecord.getTimestamp();
FormattedDate date = DateUtils.getDatelessRelativeTimeSpanFormattedDate(getContext(), locale, timestamp);
String dateLabel = date.getValue();
String dateLabelContentDesc = date.getContentDescValue();
if (displayMode != ConversationItemDisplayMode.Detailed.INSTANCE && messageRecord.isEditMessage() && messageRecord.isLatestRevision()) {
if (date.isNow()) {
dateLabel = getContext().getString(R.string.ConversationItem_edited_now_timestamp_footer);
dateLabelContentDesc = dateLabel;
} else if (date.isRelative()) {
dateLabel = getContext().getString(R.string.ConversationItem_edited_relative_timestamp_footer, date.getValue());
dateLabelContentDesc = getContext().getString(R.string.ConversationItem_edited_relative_timestamp_footer, date.getContentDescValue());
} else {
dateLabel = getContext().getString(R.string.ConversationItem_edited_absolute_timestamp_footer, date.getValue());
dateLabelContentDesc = dateLabel;
}
}
dateView.setText(dateLabel);
dateView.setContentDescription(dateLabelContentDesc);
}
}

View File

@@ -168,6 +168,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
customPref(
ValueClickPreference(
value = DSLSettingsText.from(ExpirationUtil.getExpirationAbbreviatedDisplayValue(requireContext(), state.universalExpireTimer)),
contentDescription = DSLSettingsText.from(ExpirationUtil.getExpirationDisplayValue(requireContext(), state.universalExpireTimer)),
clickPreference = ClickPreference(
title = DSLSettingsText.from(R.string.PrivacySettingsFragment__default_timer_for_new_changes),
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__set_a_default_disappearing_message_timer_for_all_new_chats_started_by_you),
@@ -364,6 +365,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
private class ValueClickPreference(
val value: DSLSettingsText,
val contentDescription: DSLSettingsText,
val clickPreference: ClickPreference
) : PreferenceModel<ValueClickPreference>(
title = clickPreference.title,
@@ -386,6 +388,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
super.bind(model)
clickPreferenceViewHolder.bind(model.clickPreference)
valueText.text = model.value.resolve(context)
valueText.contentDescription = model.contentDescription.resolve(context)
}
}

View File

@@ -165,7 +165,8 @@ public class ConversationMessage {
}
public static @NonNull FormattedDate getFormattedDate(@NonNull Context context, @NonNull MessageRecord messageRecord) {
return MessageRecordUtil.isScheduled(messageRecord) ? new FormattedDate(false, false, DateUtils.getOnlyTimeString(context, ((MmsMessageRecord) messageRecord).getScheduledDate()))
String time = DateUtils.getOnlyTimeString(context, ((MmsMessageRecord) messageRecord).getScheduledDate());
return MessageRecordUtil.isScheduled(messageRecord) ? new FormattedDate(false, false, time, time)
: DateUtils.getDatelessRelativeTimeSpanFormattedDate(context, Locale.getDefault(), messageRecord.getTimestamp());
}

View File

@@ -95,6 +95,7 @@ public class ConversationTitleView extends ConstraintLayout {
isSelf = recipient.isSelf();
expirationBadgeTime.setText(ExpirationUtil.getExpirationAbbreviatedDisplayValue(getContext(), recipient.getExpiresInSeconds()));
expirationBadgeTime.setContentDescription(ExpirationUtil.getExpirationDisplayValue(getContext(), recipient.getExpiresInSeconds()));
expirationBadgeContainer.setVisibility(View.VISIBLE);
updateSubtitleVisibility();
}

View File

@@ -87,7 +87,7 @@ class ChatColorPreviewView @JvmOverloads constructor(
findViewById(R.id.bubble_4_delivery)
)
val now: String = DateUtils.getExtendedRelativeTimeSpanString(context, Locale.getDefault(), System.currentTimeMillis())
val (now, _) = DateUtils.getExtendedRelativeTimeSpanString(context, Locale.getDefault(), System.currentTimeMillis())
listOf(sent1, sent2, recv1, recv2).forEach {
it.time.text = now
it.delivery?.setRead()

View File

@@ -8,5 +8,6 @@ package org.thoughtcrime.securesms.conversation.v2.computed
data class FormattedDate(
val isRelative: Boolean,
val isNow: Boolean,
val value: String
val value: String,
val contentDescValue: String
)

View File

@@ -709,13 +709,17 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
binding.footerDate.text = conversationMessage.computedProperties.formattedDate.value
} else {
var dateLabel = conversationMessage.computedProperties.formattedDate.value
var dateLabelContentDesc = conversationMessage.computedProperties.formattedDate.contentDescValue
if (conversationContext.displayMode != ConversationItemDisplayMode.Detailed && record is MmsMessageRecord && record.isEditMessage) {
dateLabel = if (conversationMessage.computedProperties.formattedDate.isNow) {
getContext().getString(R.string.ConversationItem_edited_now_timestamp_footer)
if (conversationMessage.computedProperties.formattedDate.isNow) {
dateLabel = getContext().getString(R.string.ConversationItem_edited_now_timestamp_footer)
dateLabelContentDesc = dateLabel
} else if (conversationMessage.computedProperties.formattedDate.isRelative) {
getContext().getString(R.string.ConversationItem_edited_relative_timestamp_footer, dateLabel)
dateLabel = getContext().getString(R.string.ConversationItem_edited_relative_timestamp_footer, dateLabel)
dateLabelContentDesc = getContext().getString(R.string.ConversationItem_edited_relative_timestamp_footer, dateLabelContentDesc)
} else {
getContext().getString(R.string.ConversationItem_edited_absolute_timestamp_footer, dateLabel)
dateLabelContentDesc = dateLabel
}
binding.footerDate.setOnClickListener {
@@ -728,6 +732,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
}
binding.footerDate.text = dateLabel
binding.footerDate.contentDescription = dateLabelContentDesc
}
}

View File

@@ -97,6 +97,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import kotlin.Pair;
import static org.thoughtcrime.securesms.database.model.LiveUpdateMessage.recipientToStringAsync;
@@ -270,8 +271,9 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
: ContextCompat.getColor(getContext(), R.color.signal_text_primary));
updateDateView = () -> {
CharSequence date = DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, thread.getDate());
dateView.setText(date);
Pair<String, String> date = DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, thread.getDate());
dateView.setText(date.getFirst());
dateView.setContentDescription(date.getSecond());
};
updateDateView.run();
@@ -318,7 +320,11 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
fromView.setText(recipient.get(), recipient.get().getDisplayName(getContext()), null, false);
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, searchStyleFactory, messageResult.getBodySnippet(), highlightSubstring, SearchUtil.MATCH_ALL));
updateDateView = () -> dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, messageResult.getReceivedTimestampMs()));
updateDateView = () -> {
Pair<String, String> date = DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, messageResult.getReceivedTimestampMs());
dateView.setText(date.getFirst());
dateView.setContentDescription(date.getSecond());
};
updateDateView.run();
archivedView.setVisibility(GONE);
@@ -353,7 +359,9 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
updateDateView = () -> {
if (groupWithMembers.getDate() > 0) {
dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, groupWithMembers.getDate()));
Pair<String, String> date = DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, groupWithMembers.getDate());
dateView.setText(date.getFirst());
dateView.setContentDescription(date.getSecond());
} else {
dateView.setText("");
}

View File

@@ -274,8 +274,11 @@ class MediaPreviewV2Fragment :
}
private fun bindTextViews(currentItem: MediaTable.MediaRecord, showThread: Boolean, messageBodies: Map<Long, SpannableString>) {
binding.toolbar.title = getTitleText(currentItem, showThread)
binding.toolbar.subtitle = getSubTitleText(currentItem)
val title = getTitleText(currentItem, showThread)
val (subtitle, subtitleContentDesc) = getSubTitleText(currentItem)
binding.toolbar.title = title
binding.toolbar.subtitle = subtitle
binding.toolbar.contentDescription = "$title $subtitleContentDesc"
val messageId: Long? = currentItem.attachment?.mmsId
if (messageId != null) {
binding.toolbar.setOnClickListener { v ->
@@ -469,11 +472,11 @@ class MediaPreviewV2Fragment :
}
}
private fun getSubTitleText(mediaRecord: MediaTable.MediaRecord): CharSequence {
val text = if (mediaRecord.date > 0) {
private fun getSubTitleText(mediaRecord: MediaTable.MediaRecord): Pair<CharSequence, CharSequence> {
val (text, contentDesc) = if (mediaRecord.date > 0) {
DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.getDefault(), mediaRecord.date)
} else {
getString(R.string.MediaPreviewActivity_draft)
Pair(getString(R.string.MediaPreviewActivity_draft), getString(R.string.MediaPreviewActivity_draft))
}
val builder = SpannableStringBuilder(text)
@@ -482,7 +485,7 @@ class MediaPreviewV2Fragment :
chevron.colorFilter = PorterDuffColorFilter(onSurfaceColor, PorterDuff.Mode.SRC_IN)
SpanUtil.appendCenteredImageSpan(builder, chevron, 10, 10)
return builder
return Pair(builder, contentDesc)
}
private fun anchorMarginsToBottomInsets(viewToAnchor: View) {

View File

@@ -41,7 +41,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.JavaTimeExtensionsKt;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -50,6 +49,8 @@ import java.time.LocalTime;
import java.util.Locale;
import java.util.Objects;
import kotlin.Pair;
public class BackupsPreferenceFragment extends Fragment {
private static final String TAG = Log.tag(BackupsPreferenceFragment.class);
@@ -187,7 +188,9 @@ public class BackupsPreferenceFragment extends Fragment {
}
private void setBackupSummary() {
summary.setText(getString(R.string.BackupsPreferenceFragment__last_backup, BackupUtil.getLastBackupTime(requireContext(), Locale.getDefault())));
Pair<String, String> date = BackupUtil.getLastBackupTime(requireContext(), Locale.getDefault());
summary.setText(getString(R.string.BackupsPreferenceFragment__last_backup, date.getFirst()));
summary.setContentDescription(getString(R.string.BackupsPreferenceFragment__last_backup, date.getSecond()));
}
private void setBackupFolderName() {

View File

@@ -303,7 +303,9 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
}
if (backupTimestamp > 0) {
binding.backupCreatedText.text = getString(R.string.RegistrationActivity_backup_timestamp_s, DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.getDefault(), backupTimestamp))
val (date, dateContentDesc) = DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.getDefault(), backupTimestamp)
binding.backupCreatedText.text = getString(R.string.RegistrationActivity_backup_timestamp_s, date)
binding.backupCreatedText.contentDescription = getString(R.string.RegistrationActivity_backup_timestamp_s, dateContentDesc)
}
}

View File

@@ -264,7 +264,9 @@ object StoriesLandingItem {
date.text = SpanUtil.color(ContextCompat.getColor(context, R.color.signal_alert_primary), context.getString(message))
} else {
errorIndicator.visible = false
date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.data.dateInMilliseconds)
val (dateString, dateContentDescription) = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.data.dateInMilliseconds)
date.text = dateString
date.contentDescription = dateContentDescription
}
}

View File

@@ -176,9 +176,11 @@ object MyStoriesItem {
viewCount.setText(R.string.StoriesLandingItem__partially_sent)
date.setText(R.string.StoriesLandingItem__tap_to_retry)
} else {
val (dateString, dateContentDescription) = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.distributionStory.messageRecord.dateSent)
errorIndicator.visible = false
date.visible = true
date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.distributionStory.messageRecord.dateSent)
date.text = dateString
date.contentDescription = dateContentDescription
}
}

View File

@@ -1058,9 +1058,10 @@ class StoryViewerPageFragment :
}
private fun presentDate(date: TextView, storyPost: StoryPost) {
val formattedDate = DateUtils.getBriefRelativeTimeSpanString(requireContext(), Locale.getDefault(), storyPost.dateInMilliseconds)
val (formattedDate, formattedDateContentDesc) = DateUtils.getBriefRelativeTimeSpanString(requireContext(), Locale.getDefault(), storyPost.dateInMilliseconds)
if (date.text != formattedDate) {
date.text = formattedDate
date.contentDescription = formattedDateContentDesc
}
}

View File

@@ -182,9 +182,11 @@ object StoryGroupReplyItem {
alertView.setNone()
itemView.setOnClickListener(null)
val dateText = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.sentAtMillis)
val (dateText, dateContentDesc) = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.sentAtMillis)
date.text = dateText
date.contentDescription = dateContentDesc
dateBelow.text = dateText
dateBelow.contentDescription = dateContentDesc
}
itemView.post {

View File

@@ -51,14 +51,16 @@ object StoryViewItem {
override fun bind(model: Model) {
avatarView.setAvatar(model.storyViewItemData.recipient)
nameView.text = model.storyViewItemData.recipient.getDisplayName(context)
viewedAtView.text = formatDate(model.storyViewItemData.timeViewedInMillis)
val (dateString, dateContentDesc) = formatDate(model.storyViewItemData.timeViewedInMillis)
viewedAtView.text = dateString
viewedAtView.contentDescription = dateContentDesc
itemView.setOnClickListener {
showContextMenu(model)
}
}
private fun formatDate(dateInMilliseconds: Long): String {
private fun formatDate(dateInMilliseconds: Long): Pair<String, String> {
return DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), dateInMilliseconds)
}

View File

@@ -31,21 +31,23 @@ import java.util.List;
import java.util.Locale;
import java.util.Objects;
import kotlin.Pair;
public class BackupUtil {
private static final String TAG = Log.tag(BackupUtil.class);
public static final int PASSPHRASE_LENGTH = 30;
public static @NonNull String getLastBackupTime(@NonNull Context context, @NonNull Locale locale) {
public static @NonNull Pair<String, String> getLastBackupTime(@NonNull Context context, @NonNull Locale locale) {
try {
BackupInfo backup = getLatestBackup();
if (backup == null) return context.getString(R.string.BackupUtil_never);
if (backup == null) return new Pair<>(context.getString(R.string.BackupUtil_never), context.getString(R.string.BackupUtil_never));
else return DateUtils.getExtendedRelativeTimeSpanString(context, locale, backup.getTimestamp());
} catch (NoExternalStorageException e) {
Log.w(TAG, e);
return context.getString(R.string.BackupUtil_unknown);
return new Pair<>(context.getString(R.string.BackupUtil_unknown), context.getString(R.string.BackupUtil_unknown));
}
}

View File

@@ -23,6 +23,7 @@ import android.text.format.DateFormat
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.v2.computed.FormattedDate
import org.thoughtcrime.securesms.util.DateUtils.getBriefRelativeTimeSpanString
import java.text.DateFormatSymbols
import java.text.ParseException
import java.text.SimpleDateFormat
@@ -59,8 +60,8 @@ object DateUtils : android.text.format.DateUtils() {
* A relative timestamp to use in space-constrained areas, like the conversation list.
*/
@JvmStatic
fun getBriefRelativeTimeSpanString(c: Context, locale: Locale, timestamp: Long): String {
return when {
fun getBriefRelativeTimeSpanString(c: Context, locale: Locale, timestamp: Long): Pair<String, String> {
val time = when {
isNow(timestamp) -> {
c.getString(R.string.DateUtils_just_now)
}
@@ -82,14 +83,15 @@ object DateUtils : android.text.format.DateUtils() {
timestamp.toDateString("MMM d, yyyy", locale)
}
}
return Pair(time, time.toAccessibleFormat(c, timestamp))
}
/**
* Similar to [getBriefRelativeTimeSpanString], except this will include additional time information in longer formats.
*/
@JvmStatic
fun getExtendedRelativeTimeSpanString(context: Context, locale: Locale, timestamp: Long): String {
return when {
fun getExtendedRelativeTimeSpanString(context: Context, locale: Locale, timestamp: Long): Pair<String, String> {
val time = when {
isNow(timestamp) -> {
context.getString(R.string.DateUtils_just_now)
}
@@ -117,6 +119,7 @@ object DateUtils : android.text.format.DateUtils() {
timestamp.toDateString(format.toString(), locale)
}
}
return Pair(time, time.toAccessibleFormat(context, timestamp))
}
/**
@@ -132,14 +135,14 @@ object DateUtils : android.text.format.DateUtils() {
fun getDatelessRelativeTimeSpanFormattedDate(context: Context, locale: Locale, timestamp: Long): FormattedDate {
return when {
isNow(timestamp) -> {
FormattedDate(isRelative = true, isNow = true, value = context.getString(R.string.DateUtils_just_now))
FormattedDate(isRelative = true, isNow = true, value = context.getString(R.string.DateUtils_just_now), contentDescValue = context.getString(R.string.DateUtils_just_now))
}
timestamp.isWithin(1.hours) -> {
val minutes = timestamp.convertDeltaTo(DurationUnit.MINUTES)
FormattedDate(isRelative = true, isNow = false, value = context.resources.getString(R.string.DateUtils_minutes_ago, minutes))
FormattedDate(isRelative = true, isNow = false, value = context.resources.getString(R.string.DateUtils_minutes_ago, minutes), contentDescValue = context.resources.getQuantityString(R.plurals.DateUtils_minutes_ago_content_description, minutes, minutes))
}
else -> {
FormattedDate(isRelative = false, isNow = false, value = getOnlyTimeString(context, timestamp))
FormattedDate(isRelative = false, isNow = false, value = getOnlyTimeString(context, timestamp), contentDescValue = getOnlyTimeString(context, timestamp))
}
}
}
@@ -428,6 +431,15 @@ object DateUtils : android.text.format.DateUtils() {
}
}
fun String.toAccessibleFormat(context: Context, timestamp: Long): String {
return if (!isNow(timestamp) && timestamp.isWithin(1.hours)) {
val minutes = timestamp.convertDeltaTo(DurationUnit.MINUTES)
context.resources.getQuantityString(R.plurals.DateUtils_minutes_ago_content_description, minutes, minutes)
} else {
this
}
}
private fun SimpleDateFormat.setLowercaseAmPmStrings(locale: Locale): SimpleDateFormat {
val symbols = dateFormatSymbolsCache.getOrPut(locale) {
DateFormatSymbols(locale).apply {