diff --git a/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.java b/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.java index c1f249ab6f..10f11dab76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ExpirationDialog.java @@ -1,14 +1,17 @@ package org.thoughtcrime.securesms; import android.content.Context; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; + import org.thoughtcrime.securesms.util.ExpirationUtil; +import java.util.Arrays; + import cn.carbswang.android.numberpickerview.library.NumberPickerView; public class ExpirationDialog extends AlertDialog { @@ -36,7 +39,7 @@ public class ExpirationDialog extends AlertDialog { builder.setView(view); builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { int selected = ((NumberPickerView)view.findViewById(R.id.expiration_number_picker)).getValue(); - listener.onClick(context.getResources().getIntArray(R.array.expiration_times)[selected]); + listener.onClick(getExpirationTimes(context, currentExpiration)[selected]); }); builder.setNegativeButton(android.R.string.cancel, null); builder.show(); @@ -47,7 +50,7 @@ public class ExpirationDialog extends AlertDialog { final View view = inflater.inflate(R.layout.expiration_dialog, null); final NumberPickerView numberPickerView = view.findViewById(R.id.expiration_number_picker); final TextView textView = view.findViewById(R.id.expiration_details); - final int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times); + final int[] expirationTimes = getExpirationTimes(context, currentExpiration); final String[] expirationDisplayValues = new String[expirationTimes.length]; int selectedIndex = expirationTimes.length - 1; @@ -80,6 +83,19 @@ public class ExpirationDialog extends AlertDialog { return view; } + private static int[] getExpirationTimes(Context context, int currentExpiration) { + int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times); + int location = Arrays.binarySearch(expirationTimes, currentExpiration); + if (location < 0) { + int[] temp = Arrays.copyOf(expirationTimes, expirationTimes.length + 1); + temp[temp.length - 1] = currentExpiration; + Arrays.sort(temp); + expirationTimes = temp; + } + + return expirationTimes; + } + public interface OnClickListener { public void onClick(int expirationTime); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ExpirationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ExpirationUtil.java index f5a83a7448..e6cfb8b442 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ExpirationUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ExpirationUtil.java @@ -2,49 +2,101 @@ package org.thoughtcrime.securesms.util; import android.content.Context; +import androidx.annotation.PluralsRes; +import androidx.annotation.StringRes; + import org.thoughtcrime.securesms.R; import java.util.concurrent.TimeUnit; -public class ExpirationUtil { +public final class ExpirationUtil { + + private static final int SECONDS_IN_WEEK = (int) TimeUnit.DAYS.toSeconds(7); + private static final int SECONDS_IN_DAY = (int) TimeUnit.DAYS.toSeconds(1); + private static final int SECONDS_IN_HOUR = (int) TimeUnit.HOURS.toSeconds(1); + private static final int SECONDS_IN_MINUTE = (int) TimeUnit.MINUTES.toSeconds(1); public static String getExpirationDisplayValue(Context context, int expirationTime) { if (expirationTime <= 0) { return context.getString(R.string.expiration_off); - } else if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) { - return context.getResources().getQuantityString(R.plurals.expiration_seconds, expirationTime, expirationTime); - } else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) { - int minutes = expirationTime / (int)TimeUnit.MINUTES.toSeconds(1); - return context.getResources().getQuantityString(R.plurals.expiration_minutes, minutes, minutes); - } else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) { - int hours = expirationTime / (int)TimeUnit.HOURS.toSeconds(1); - return context.getResources().getQuantityString(R.plurals.expiration_hours, hours, hours); - } else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) { - int days = expirationTime / (int)TimeUnit.DAYS.toSeconds(1); - return context.getResources().getQuantityString(R.plurals.expiration_days, days, days); - } else { - int weeks = expirationTime / (int)TimeUnit.DAYS.toSeconds(7); - return context.getResources().getQuantityString(R.plurals.expiration_weeks, weeks, weeks); } + + String displayValue = ""; + + int secondsRemaining = expirationTime; + + int weeks = secondsRemaining / SECONDS_IN_WEEK; + displayValue = getDisplayValue(context, displayValue, R.plurals.expiration_weeks, weeks); + secondsRemaining = secondsRemaining - weeks * SECONDS_IN_WEEK; + + int days = secondsRemaining / SECONDS_IN_DAY; + displayValue = getDisplayValue(context, displayValue, R.plurals.expiration_days, days); + secondsRemaining = secondsRemaining - days * SECONDS_IN_DAY; + + int hours = secondsRemaining / SECONDS_IN_HOUR; + displayValue = getDisplayValue(context, displayValue, R.plurals.expiration_hours, hours); + secondsRemaining = secondsRemaining - hours * SECONDS_IN_HOUR; + + int minutes = secondsRemaining / SECONDS_IN_MINUTE; + displayValue = getDisplayValue(context, displayValue, R.plurals.expiration_minutes, minutes); + secondsRemaining = secondsRemaining - minutes * SECONDS_IN_MINUTE; + + displayValue = getDisplayValue(context, displayValue, R.plurals.expiration_seconds, secondsRemaining); + + return displayValue; + } + + private static String getDisplayValue(Context context, String currentValue, @PluralsRes int plurals, int duration) { + if (duration > 0) { + String durationString = context.getResources().getQuantityString(plurals, duration, duration); + if (currentValue.isEmpty()) { + return durationString; + } else { + return context.getString(R.string.expiration_combined, currentValue, durationString); + } + } + return currentValue; } public static String getExpirationAbbreviatedDisplayValue(Context context, int expirationTime) { - if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) { - return context.getResources().getString(R.string.expiration_seconds_abbreviated, expirationTime); - } else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) { - int minutes = expirationTime / (int)TimeUnit.MINUTES.toSeconds(1); - return context.getResources().getString(R.string.expiration_minutes_abbreviated, minutes); - } else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) { - int hours = expirationTime / (int)TimeUnit.HOURS.toSeconds(1); - return context.getResources().getString(R.string.expiration_hours_abbreviated, hours); - } else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) { - int days = expirationTime / (int)TimeUnit.DAYS.toSeconds(1); - return context.getResources().getString(R.string.expiration_days_abbreviated, days); - } else { - int weeks = expirationTime / (int)TimeUnit.DAYS.toSeconds(7); - return context.getResources().getString(R.string.expiration_weeks_abbreviated, weeks); + if (expirationTime <= 0) { + return context.getString(R.string.expiration_off); } + + String displayValue = ""; + + int secondsRemaining = expirationTime; + + int weeks = secondsRemaining / SECONDS_IN_WEEK; + displayValue = getAbbreviatedDisplayValue(context, displayValue, R.string.expiration_weeks_abbreviated, weeks); + secondsRemaining = secondsRemaining - weeks * SECONDS_IN_WEEK; + + int days = secondsRemaining / SECONDS_IN_DAY; + displayValue = getAbbreviatedDisplayValue(context, displayValue, R.string.expiration_days_abbreviated, days); + secondsRemaining = secondsRemaining - days * SECONDS_IN_DAY; + + int hours = secondsRemaining / SECONDS_IN_HOUR; + displayValue = getAbbreviatedDisplayValue(context, displayValue, R.string.expiration_hours_abbreviated, hours); + secondsRemaining = secondsRemaining - hours * SECONDS_IN_HOUR; + + int minutes = secondsRemaining / SECONDS_IN_MINUTE; + displayValue = getAbbreviatedDisplayValue(context, displayValue, R.string.expiration_minutes_abbreviated, minutes); + secondsRemaining = secondsRemaining - minutes * SECONDS_IN_MINUTE; + + displayValue = getAbbreviatedDisplayValue(context, displayValue, R.string.expiration_seconds_abbreviated, secondsRemaining); + + return displayValue; } - + private static String getAbbreviatedDisplayValue(Context context, String currentValue, @StringRes int abbreviation, int duration) { + if (duration > 0) { + String durationString = context.getString(abbreviation, duration); + if (currentValue.isEmpty()) { + return durationString; + } else { + return context.getString(R.string.expiration_combined, currentValue, durationString); + } + } + return currentValue; + } } diff --git a/app/src/main/res/layout/group_manage_fragment.xml b/app/src/main/res/layout/group_manage_fragment.xml index 36084d11b7..8b5b25e237 100644 --- a/app/src/main/res/layout/group_manage_fragment.xml +++ b/app/src/main/res/layout/group_manage_fragment.xml @@ -130,32 +130,36 @@ + android:gravity="center_vertical" + android:minHeight="@dimen/group_manage_fragment_row_height" + android:orientation="horizontal" + android:paddingTop="8dp" + android:paddingBottom="8dp"> diff --git a/app/src/main/res/layout/recipient_manage_fragment.xml b/app/src/main/res/layout/recipient_manage_fragment.xml index bb99da2ddc..dce36d9ad7 100644 --- a/app/src/main/res/layout/recipient_manage_fragment.xml +++ b/app/src/main/res/layout/recipient_manage_fragment.xml @@ -226,17 +226,20 @@ + android:gravity="center_vertical" + android:minHeight="@dimen/group_manage_fragment_row_height" + android:orientation="horizontal" + android:paddingTop="8dp" + android:paddingBottom="8dp"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 29fb9ecbfc..9feb16761e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1587,6 +1587,7 @@ %dw + %1$s %2$s Your safety number with %s has changed and is no longer verified diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/ExpirationUtilTest.java b/app/src/test/java/org/thoughtcrime/securesms/util/ExpirationUtilTest.java new file mode 100644 index 0000000000..32a1cdf9b6 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/util/ExpirationUtilTest.java @@ -0,0 +1,148 @@ +package org.thoughtcrime.securesms.util; + +import android.app.Application; +import android.content.Context; +import android.content.res.Resources; +import android.util.Pair; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.thoughtcrime.securesms.R; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, application = Application.class) +public class ExpirationUtilTest { + + private static int SECONDS_IN_WEEK = (int) TimeUnit.DAYS.toSeconds(7); + private static int SECONDS_IN_DAY = (int) TimeUnit.DAYS.toSeconds(1); + private static int SECONDS_IN_HOUR = (int) TimeUnit.HOURS.toSeconds(1); + private static int SECONDS_IN_MINUTE = (int) TimeUnit.MINUTES.toSeconds(1); + + private Context context; + + @Before + public void setup() { + context = ApplicationProvider.getApplicationContext(); + } + + @Test + public void shouldFormatAsSeconds_whenEvenSecondUnderMinute() { + assertEquals(1 + " second", ExpirationUtil.getExpirationDisplayValue(context, 1)); + for (int seconds = 2; seconds < 60; seconds++) { + assertEquals(seconds + " seconds", ExpirationUtil.getExpirationDisplayValue(context, seconds)); + } + } + + @Test + public void shouldFormatAsMinutes_whenEvenMinuteUnderHour() { + assertEquals(1 + " minute", ExpirationUtil.getExpirationDisplayValue(context, (int) TimeUnit.MINUTES.toSeconds(1))); + for (int minutes = 2; minutes < 60; minutes++) { + assertEquals(minutes + " minutes", ExpirationUtil.getExpirationDisplayValue(context, (int) TimeUnit.MINUTES.toSeconds(minutes))); + } + } + + @Test + public void shouldFormatAsHours_whenEvenHourUnderDay() { + assertEquals(1 + " hour", ExpirationUtil.getExpirationDisplayValue(context, (int) TimeUnit.HOURS.toSeconds(1))); + for (int hours = 2; hours < 24; hours++) { + assertEquals(hours + " hours", ExpirationUtil.getExpirationDisplayValue(context, (int) TimeUnit.HOURS.toSeconds(hours))); + } + } + + @Test + public void shouldFormatAsDays_whenEvenDayUnderWeek() { + assertEquals(1 + " day", ExpirationUtil.getExpirationDisplayValue(context, (int) TimeUnit.DAYS.toSeconds(1))); + for (int days = 2; days < 7; days++) { + assertEquals(days + " days", ExpirationUtil.getExpirationDisplayValue(context, (int) TimeUnit.DAYS.toSeconds(days))); + } + } + + @Test + public void shouldFormatAsWeeks_whenEvenWeek() { + assertEquals(1 + " week", ExpirationUtil.getExpirationDisplayValue(context, (int) TimeUnit.DAYS.toSeconds(7))); + for (int weeks = 2; weeks < 52; weeks++) { + assertEquals(weeks + " weeks", ExpirationUtil.getExpirationDisplayValue(context, (int) TimeUnit.DAYS.toSeconds(7 * weeks))); + } + } + + @Test + public void shouldFormatAsBreakdown_whenLargerThanWeek() { + assertEquals("1 week 1 day", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_WEEK + SECONDS_IN_DAY)); + + assertEquals("1 week 1 hour", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_WEEK + SECONDS_IN_HOUR)); + + assertEquals("1 week 1 minute", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_WEEK + SECONDS_IN_MINUTE)); + + assertEquals("1 week 1 second", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_WEEK + 1)); + + assertEquals("1 week 1 day 1 hour", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_WEEK + SECONDS_IN_DAY + SECONDS_IN_HOUR)); + + assertEquals("1 week 1 day 1 hour 1 minute", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_WEEK + SECONDS_IN_DAY + SECONDS_IN_HOUR + SECONDS_IN_MINUTE)); + + assertEquals("1 week 1 day 1 hour 1 minute 1 second", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_WEEK + SECONDS_IN_DAY + SECONDS_IN_HOUR + SECONDS_IN_MINUTE + 1)); + + assertEquals("1 week 1 hour 1 minute", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_WEEK + SECONDS_IN_HOUR + SECONDS_IN_MINUTE)); + + assertEquals("1 week 1 hour 1 minute 1 second", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_WEEK + SECONDS_IN_HOUR + SECONDS_IN_MINUTE + 1)); + + assertEquals("1 week 1 minute 1 second", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_WEEK + SECONDS_IN_MINUTE + 1)); + } + + @Test + public void shouldFormatAsBreakdown_whenLargerThanDay() { + assertEquals("1 day 1 hour", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_DAY + SECONDS_IN_HOUR)); + + assertEquals("1 day 1 minute", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_DAY + SECONDS_IN_MINUTE)); + + assertEquals("1 day 1 second", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_DAY + 1)); + } + + @Test + public void shouldFormatAsBreakdown_whenLargerThanHour() { + assertEquals("1 hour 1 minute", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_HOUR + SECONDS_IN_MINUTE)); + + assertEquals("1 hour 1 second", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_HOUR + 1)); + } + + @Test + public void shouldFormatAsBreakdown_whenLargerThanMinute() { + assertEquals("1 minute 1 second", + ExpirationUtil.getExpirationDisplayValue(context, SECONDS_IN_MINUTE + 1)); + } +}