Reland "Don't use framework strings for formatting file sizes"

Reland http://ag/2529020 because the over-translation issue should be
resolved now.

Modify various formatters to avoid using framework strings for
formatting file sizes.

Also update README instructions for running unit tests.

Bug: 36994779
Bug: 71580745
Bug: 217592956
Test: no new test failures from RunSettingsRoboTests
Test: manual opening the settings page.
Change-Id: Ic4689ab1b76622028004d05e69858228bdc441cf
diff --git a/res/layout/data_usage_bytes_editor.xml b/res/layout/data_usage_bytes_editor.xml
index a72352d..bcfc25e 100644
--- a/res/layout/data_usage_bytes_editor.xml
+++ b/res/layout/data_usage_bytes_editor.xml
@@ -38,7 +38,6 @@
         android:id="@+id/size_spinner"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
-        android:layout_gravity="center_vertical"
-        android:entries="@array/bytes_picker_sizes" />
+        android:layout_gravity="center_vertical" />
 
 </LinearLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index e7f9990..8be1a97 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -11307,11 +11307,6 @@
     <!-- Text for the setting on whether you can type text into notifications without unlocking the device. -->
     <string name="lockscreen_remote_input">If device is locked, prevent typing replies or other text in notifications</string>
 
-    <string-array name="bytes_picker_sizes" translatable="false">
-        <item>@*android:string/megabyteShort</item>
-        <item>@*android:string/gigabyteShort</item>
-    </string-array>
-
     <!-- [CHAR LIMIT=30] Label for setting to control the default spell checker -->
     <string name="default_spell_checker">Default spell checker</string>
 
diff --git a/src/com/android/settings/datausage/BillingCycleSettings.java b/src/com/android/settings/datausage/BillingCycleSettings.java
index 57931c1..9e83e4c 100644
--- a/src/com/android/settings/datausage/BillingCycleSettings.java
+++ b/src/com/android/settings/datausage/BillingCycleSettings.java
@@ -22,6 +22,8 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.res.Resources;
+import android.icu.text.MeasureFormat;
+import android.icu.util.MeasureUnit;
 import android.net.NetworkPolicy;
 import android.net.NetworkTemplate;
 import android.os.Bundle;
@@ -30,6 +32,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.inputmethod.EditorInfo;
+import android.widget.ArrayAdapter;
 import android.widget.EditText;
 import android.widget.NumberPicker;
 import android.widget.Spinner;
@@ -301,6 +304,17 @@
                     : editor.getPolicyWarningBytes(template);
             final long limitDisabled = isLimit ? LIMIT_DISABLED : WARNING_DISABLED;
 
+            final MeasureFormat formatter = MeasureFormat.getInstance(
+                    getContext().getResources().getConfiguration().locale,
+                    MeasureFormat.FormatWidth.SHORT);
+            final String[] unitNames = new String[] {
+                formatter.getUnitDisplayName(MeasureUnit.MEGABYTE),
+                formatter.getUnitDisplayName(MeasureUnit.GIGABYTE)
+            };
+            final ArrayAdapter<String> adapter = new ArrayAdapter<String>(
+                    getContext(), android.R.layout.simple_spinner_item, unitNames);
+            type.setAdapter(adapter);
+
             final boolean unitInGigaBytes = (bytes > 1.5f * GIB_IN_BYTES);
             final String bytesText = formatText(bytes,
                     unitInGigaBytes ? GIB_IN_BYTES : MIB_IN_BYTES);
diff --git a/src/com/android/settings/utils/FileSizeFormatter.java b/src/com/android/settings/utils/FileSizeFormatter.java
index e56388a..9ef0fe2 100644
--- a/src/com/android/settings/utils/FileSizeFormatter.java
+++ b/src/com/android/settings/utils/FileSizeFormatter.java
@@ -16,11 +16,20 @@
 
 package com.android.settings.utils;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.content.res.Resources;
+import android.icu.text.DecimalFormat;
+import android.icu.text.MeasureFormat;
+import android.icu.text.NumberFormat;
+import android.icu.util.Measure;
+import android.icu.util.MeasureUnit;
 import android.text.BidiFormatter;
-import android.text.format.Formatter;
+import android.text.TextUtils;
+import android.view.View;
+
+import java.math.BigDecimal;
+import java.util.Locale;
 
 /**
  * Utility class to aid in formatting file sizes always with the same unit. This is modified from
@@ -31,6 +40,61 @@
     public static final long MEGABYTE_IN_BYTES = KILOBYTE_IN_BYTES * 1000;
     public static final long GIGABYTE_IN_BYTES = MEGABYTE_IN_BYTES * 1000;
 
+    private static class RoundedBytesResult {
+        public final float value;
+        public final MeasureUnit units;
+        public final int fractionDigits;
+        public final long roundedBytes;
+
+        private RoundedBytesResult(
+                float value, MeasureUnit units, int fractionDigits, long roundedBytes) {
+            this.value = value;
+            this.units = units;
+            this.fractionDigits = fractionDigits;
+            this.roundedBytes = roundedBytes;
+        }
+    }
+
+    private static Locale localeFromContext(@NonNull Context context) {
+        return context.getResources().getConfiguration().locale;
+    }
+
+    private static String bidiWrap(@NonNull Context context, String source) {
+        final Locale locale = localeFromContext(context);
+        if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL) {
+            return BidiFormatter.getInstance(true /* RTL*/).unicodeWrap(source);
+        } else {
+            return source;
+        }
+    }
+
+    private static NumberFormat getNumberFormatter(Locale locale, int fractionDigits) {
+        final NumberFormat numberFormatter = NumberFormat.getInstance(locale);
+        numberFormatter.setMinimumFractionDigits(fractionDigits);
+        numberFormatter.setMaximumFractionDigits(fractionDigits);
+        numberFormatter.setGroupingUsed(false);
+        if (numberFormatter instanceof DecimalFormat) {
+            // We do this only for DecimalFormat, since in the general NumberFormat case, calling
+            // setRoundingMode may throw an exception.
+            numberFormatter.setRoundingMode(BigDecimal.ROUND_HALF_UP);
+        }
+        return numberFormatter;
+    }
+
+    private static String formatMeasureShort(Locale locale, NumberFormat numberFormatter,
+            float value, MeasureUnit units) {
+        final MeasureFormat measureFormatter = MeasureFormat.getInstance(
+                locale, MeasureFormat.FormatWidth.SHORT, numberFormatter);
+        return measureFormatter.format(new Measure(value, units));
+    }
+
+    private static String formatRoundedBytesResult(
+            @NonNull Context context, @NonNull RoundedBytesResult input) {
+        final Locale locale = localeFromContext(context);
+        final NumberFormat numberFormatter = getNumberFormatter(locale, input.fractionDigits);
+        return formatMeasureShort(locale, numberFormatter, input.value, input.units);
+    }
+
     /**
      * Formats a content size to be in the form of bytes, kilobytes, megabytes, etc.
      *
@@ -47,23 +111,17 @@
      *
      * @param context Context to use to load the localized units
      * @param sizeBytes size value to be formatted, in bytes
-     * @param suffix String id for the unit suffix.
-     * @param mult Amount of bytes in the unit. * @return formatted string with the number
+     * @param unit The unit used for formatting.
+     * @param mult Amount of bytes in the unit.
+     * @return formatted string with the number
      */
     public static String formatFileSize(
-            @Nullable Context context, long sizeBytes, int suffix, long mult) {
+            @Nullable Context context, long sizeBytes, MeasureUnit unit, long mult) {
         if (context == null) {
             return "";
         }
-        final Formatter.BytesResult res =
-                formatBytes(context.getResources(), sizeBytes, suffix, mult);
-        return BidiFormatter.getInstance()
-                .unicodeWrap(context.getString(getFileSizeSuffix(context), res.value, res.units));
-    }
-
-    private static int getFileSizeSuffix(Context context) {
-        final Resources res = context.getResources();
-        return res.getIdentifier("fileSizeSuffix", "string", "android");
+        final RoundedBytesResult res = formatBytes(sizeBytes, unit, mult);
+        return bidiWrap(context, formatRoundedBytesResult(context, res));
     }
 
     /**
@@ -76,8 +134,8 @@
      * @param suffix String id for the unit suffix.
      * @param mult Amount of bytes in the unit.
      */
-    private static Formatter.BytesResult formatBytes(
-            Resources res, long sizeBytes, int suffix, long mult) {
+    private static RoundedBytesResult formatBytes(
+            long sizeBytes, MeasureUnit unit, long mult) {
         final boolean isNegative = (sizeBytes < 0);
         float result = isNegative ? -sizeBytes : sizeBytes;
         result = result / mult;
@@ -85,32 +143,29 @@
         // compute the rounded value. String.format("%f", 0.1) might not return "0.1" due to
         // floating point errors.
         final int roundFactor;
-        final String roundFormat;
+        final int roundDigits;
         if (mult == 1) {
             roundFactor = 1;
-            roundFormat = "%.0f";
+            roundDigits = 0;
         } else if (result < 1) {
             roundFactor = 100;
-            roundFormat = "%.2f";
+            roundDigits = 2;
         } else if (result < 10) {
             roundFactor = 10;
-            roundFormat = "%.1f";
+            roundDigits = 1;
         } else { // 10 <= result < 100
             roundFactor = 1;
-            roundFormat = "%.0f";
+            roundDigits = 0;
         }
 
         if (isNegative) {
             result = -result;
         }
-        final String roundedString = String.format(roundFormat, result);
 
         // Note this might overflow if abs(result) >= Long.MAX_VALUE / 100, but that's like 80PB so
         // it's okay (for now)...
         final long roundedBytes = (((long) Math.round(result * roundFactor)) * mult / roundFactor);
 
-        final String units = res.getString(suffix);
-
-        return new Formatter.BytesResult(roundedString, units, roundedBytes);
+        return new RoundedBytesResult(result, unit, roundDigits, roundedBytes);
     }
 }
diff --git a/tests/legacy_unit/src/com/android/settings/utils/FileSizeFormatterTest.java b/tests/legacy_unit/src/com/android/settings/utils/FileSizeFormatterTest.java
index a255d0b..953bd34 100644
--- a/tests/legacy_unit/src/com/android/settings/utils/FileSizeFormatterTest.java
+++ b/tests/legacy_unit/src/com/android/settings/utils/FileSizeFormatterTest.java
@@ -18,9 +18,11 @@
 
 import static com.android.settings.utils.FileSizeFormatter.GIGABYTE_IN_BYTES;
 import static com.android.settings.utils.FileSizeFormatter.MEGABYTE_IN_BYTES;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Context;
+import android.icu.util.MeasureUnit;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -46,7 +48,7 @@
                         FileSizeFormatter.formatFileSize(
                                 mContext,
                                 0 /* size */,
-                                com.android.internal.R.string.gigabyteShort,
+                                MeasureUnit.GIGABYTE,
                                 GIGABYTE_IN_BYTES))
                 .isEqualTo("0.00 GB");
     }
@@ -57,7 +59,7 @@
                         FileSizeFormatter.formatFileSize(
                                 mContext,
                                 MEGABYTE_IN_BYTES * 11 /* size */,
-                                com.android.internal.R.string.gigabyteShort,
+                                MeasureUnit.GIGABYTE,
                                 GIGABYTE_IN_BYTES))
                 .isEqualTo("0.01 GB");
     }
@@ -68,7 +70,7 @@
                         FileSizeFormatter.formatFileSize(
                                 mContext,
                                 MEGABYTE_IN_BYTES * 155 /* size */,
-                                com.android.internal.R.string.gigabyteShort,
+                                MeasureUnit.GIGABYTE,
                                 GIGABYTE_IN_BYTES))
                 .isEqualTo("0.16 GB");
     }
@@ -79,7 +81,7 @@
                         FileSizeFormatter.formatFileSize(
                                 mContext,
                                 MEGABYTE_IN_BYTES * 1551 /* size */,
-                                com.android.internal.R.string.gigabyteShort,
+                                MeasureUnit.GIGABYTE,
                                 GIGABYTE_IN_BYTES))
                 .isEqualTo("1.6 GB");
     }
@@ -91,7 +93,7 @@
                         FileSizeFormatter.formatFileSize(
                                 mContext,
                                 GIGABYTE_IN_BYTES * 15 + MEGABYTE_IN_BYTES * 50 /* size */,
-                                com.android.internal.R.string.gigabyteShort,
+                                MeasureUnit.GIGABYTE,
                                 GIGABYTE_IN_BYTES))
                 .isEqualTo("15 GB");
     }
@@ -102,7 +104,7 @@
                         FileSizeFormatter.formatFileSize(
                                 mContext,
                                 MEGABYTE_IN_BYTES * -155 /* size */,
-                                com.android.internal.R.string.gigabyteShort,
+                                MeasureUnit.GIGABYTE,
                                 GIGABYTE_IN_BYTES))
                 .isEqualTo("-0.16 GB");
     }
diff --git a/tests/unit/README b/tests/unit/README
index 5a85603..262c8bf 100644
--- a/tests/unit/README
+++ b/tests/unit/README
@@ -7,14 +7,14 @@
 // The following instrutions show how to run the test suite using make + adb //
 
 To build the tests you can use the following command at the root of your android source tree
-$ make SettingsUnitTests
+$ make -j SettingsUnitTests
 
 The test apk then needs to be installed onto your test device. The apk's location will vary
 depending on your device model and architecture. At the end of the make command's output, there
 should be a line similar to the following:
-"Copy: out/target/product/shamu/testcases/SettingsUnitTests/arm64/SettingsUnitTests.apk"
+"Copy: ${ANDROID_PRODUCT_OUT}/testcases/SettingsUnitTests/arm64/SettingsUnitTests.apk"
 Install via the following command:
-$ adb install -r out/target/product/shamu/testcases/SettingsUnitTests/arm64/SettingsUnitTests.apk
+$ adb install -r ${ANDROID_PRODUCT_OUT}/testcases/SettingsUnitTests/arm64/SettingsUnitTests.apk
 
 To run all tests:
 $ adb shell am instrument -w com.android.settings.tests.unit/androidx.test.runner.AndroidJUnitRunner