Don't use framework strings for formatting file sizes

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

Also update README instructions for running unit tests.

Bug: 36994779
Test: adb shell am instrument -w -e class com.android.settings.utils.FileSizeFormatterTest com.android.settings.tests.unit/android.support.test.runner.AndroidJUnitRunner
Test: make -j RunSettingsRoboTests
Change-Id: I4035f26d29408b64389892a4a2379b4823f8ac96
This commit is contained in:
Roozbeh Pournader
2017-07-12 11:37:59 -07:00
parent e7c20d6fc4
commit ded99003c4
11 changed files with 116 additions and 55 deletions

View File

@@ -37,7 +37,6 @@
android:id="@+id/size_spinner" android:id="@+id/size_spinner"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical" />
android:entries="@array/bytes_picker_sizes" />
</LinearLayout> </LinearLayout>

View File

@@ -8328,11 +8328,6 @@
<!-- Text for the setting on whether you can type text into notifications without unlocking the device. --> <!-- 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 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 --> <!-- [CHAR LIMIT=30] Label for setting to control the default spell checker -->
<string name="default_spell_checker">Default spell checker</string> <string name="default_spell_checker">Default spell checker</string>

View File

@@ -21,6 +21,8 @@ import android.app.Fragment;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.res.Resources; import android.content.res.Resources;
import android.icu.text.MeasureFormat;
import android.icu.util.MeasureUnit;
import android.net.NetworkPolicy; import android.net.NetworkPolicy;
import android.net.NetworkTemplate; import android.net.NetworkTemplate;
import android.os.Bundle; import android.os.Bundle;
@@ -31,6 +33,7 @@ import android.text.format.Time;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText; import android.widget.EditText;
import android.widget.NumberPicker; import android.widget.NumberPicker;
import android.widget.Spinner; import android.widget.Spinner;
@@ -248,6 +251,17 @@ public class BillingCycleSettings extends DataUsageBase implements
: editor.getPolicyWarningBytes(template); : editor.getPolicyWarningBytes(template);
final long limitDisabled = isLimit ? LIMIT_DISABLED : WARNING_DISABLED; 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);
if (bytes > 1.5f * GB_IN_BYTES) { if (bytes > 1.5f * GB_IN_BYTES) {
final String bytesText = formatText(bytes / (float) GB_IN_BYTES); final String bytesText = formatText(bytes / (float) GB_IN_BYTES);
bytesPicker.setText(bytesText); bytesPicker.setText(bytesText);

View File

@@ -18,6 +18,7 @@ package com.android.settings.deviceinfo;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.icu.util.MeasureUnit;
import android.support.v7.preference.Preference; import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceViewHolder; import android.support.v7.preference.PreferenceViewHolder;
import android.util.AttributeSet; import android.util.AttributeSet;
@@ -51,7 +52,7 @@ public class StorageItemPreference extends Preference {
FileSizeFormatter.formatFileSize( FileSizeFormatter.formatFileSize(
getContext(), getContext(),
size, size,
getGigabyteSuffix(getContext().getResources()), MeasureUnit.GIGABYTE,
FileSizeFormatter.GIGABYTE_IN_BYTES)); FileSizeFormatter.GIGABYTE_IN_BYTES));
if (total == 0) { if (total == 0) {
mProgressPercent = 0; mProgressPercent = 0;
@@ -75,8 +76,4 @@ public class StorageItemPreference extends Preference {
updateProgressBar(); updateProgressBar();
super.onBindViewHolder(view); super.onBindViewHolder(view);
} }
private static int getGigabyteSuffix(Resources res) {
return res.getIdentifier("gigabyteShort", "string", "android");
}
} }

View File

@@ -16,11 +16,22 @@
package com.android.settings.utils; package com.android.settings.utils;
import android.annotation.NonNull;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; 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.BidiFormatter;
import android.text.TextUtils;
import android.text.format.Formatter; import android.text.format.Formatter;
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 * Utility class to aid in formatting file sizes always with the same unit. This is modified from
@@ -31,6 +42,61 @@ public final class FileSizeFormatter {
public static final long MEGABYTE_IN_BYTES = KILOBYTE_IN_BYTES * 1000; public static final long MEGABYTE_IN_BYTES = KILOBYTE_IN_BYTES * 1000;
public static final long GIGABYTE_IN_BYTES = MEGABYTE_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;
public 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. * Formats a content size to be in the form of bytes, kilobytes, megabytes, etc.
* *
@@ -47,23 +113,17 @@ public final class FileSizeFormatter {
* *
* @param context Context to use to load the localized units * @param context Context to use to load the localized units
* @param sizeBytes size value to be formatted, in bytes * @param sizeBytes size value to be formatted, in bytes
* @param suffix String id for the unit suffix. * @param unit The unit used for formatting.
* @param mult Amount of bytes in the unit. * @return formatted string with the number * @param mult Amount of bytes in the unit.
* @return formatted string with the number
*/ */
public static String formatFileSize( 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) { if (context == null) {
return ""; return "";
} }
final Formatter.BytesResult res = final RoundedBytesResult res = formatBytes(sizeBytes, unit, mult);
formatBytes(context.getResources(), sizeBytes, suffix, mult); return bidiWrap(context, formatRoundedBytesResult(context, res));
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");
} }
/** /**
@@ -76,8 +136,8 @@ public final class FileSizeFormatter {
* @param suffix String id for the unit suffix. * @param suffix String id for the unit suffix.
* @param mult Amount of bytes in the unit. * @param mult Amount of bytes in the unit.
*/ */
private static Formatter.BytesResult formatBytes( private static RoundedBytesResult formatBytes(
Resources res, long sizeBytes, int suffix, long mult) { long sizeBytes, MeasureUnit unit, long mult) {
final boolean isNegative = (sizeBytes < 0); final boolean isNegative = (sizeBytes < 0);
float result = isNegative ? -sizeBytes : sizeBytes; float result = isNegative ? -sizeBytes : sizeBytes;
result = result / mult; result = result / mult;
@@ -85,32 +145,29 @@ public final class FileSizeFormatter {
// compute the rounded value. String.format("%f", 0.1) might not return "0.1" due to // compute the rounded value. String.format("%f", 0.1) might not return "0.1" due to
// floating point errors. // floating point errors.
final int roundFactor; final int roundFactor;
final String roundFormat; final int roundDigits;
if (mult == 1) { if (mult == 1) {
roundFactor = 1; roundFactor = 1;
roundFormat = "%.0f"; roundDigits = 0;
} else if (result < 1) { } else if (result < 1) {
roundFactor = 100; roundFactor = 100;
roundFormat = "%.2f"; roundDigits = 2;
} else if (result < 10) { } else if (result < 10) {
roundFactor = 10; roundFactor = 10;
roundFormat = "%.1f"; roundDigits = 1;
} else { // 10 <= result < 100 } else { // 10 <= result < 100
roundFactor = 1; roundFactor = 1;
roundFormat = "%.0f"; roundDigits = 0;
} }
if (isNegative) { if (isNegative) {
result = -result; 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 // Note this might overflow if abs(result) >= Long.MAX_VALUE / 100, but that's like 80PB so
// it's okay (for now)... // it's okay (for now)...
final long roundedBytes = (((long) Math.round(result * roundFactor)) * mult / roundFactor); final long roundedBytes = (((long) Math.round(result * roundFactor)) * mult / roundFactor);
final String units = res.getString(suffix); return new RoundedBytesResult(result, unit, roundDigits, roundedBytes);
return new Formatter.BytesResult(roundedString, units, roundedBytes);
} }
} }

View File

@@ -55,7 +55,7 @@ public class StorageItemPreferenceTest {
@Test @Test
public void testAfterLoad() { public void testAfterLoad() {
mPreference.setStorageSize(MEGABYTE_IN_BYTES * 10, MEGABYTE_IN_BYTES * 100); mPreference.setStorageSize(MEGABYTE_IN_BYTES * 10, MEGABYTE_IN_BYTES * 100);
assertThat(((String) mPreference.getSummary())).isEqualTo("0.01GB"); assertThat(((String) mPreference.getSummary())).isEqualTo("0.01 GB");
} }
@Test @Test

View File

@@ -104,7 +104,7 @@ public class SecondaryUserControllerTest {
verify(mGroup).addPreference(argumentCaptor.capture()); verify(mGroup).addPreference(argumentCaptor.capture());
Preference preference = argumentCaptor.getValue(); Preference preference = argumentCaptor.getValue();
assertThat(preference.getSummary()).isEqualTo("0.01GB"); assertThat(preference.getSummary()).isEqualTo("0.01 GB");
} }
@Test @Test
@@ -177,7 +177,7 @@ public class SecondaryUserControllerTest {
verify(mGroup).addPreference(argumentCaptor.capture()); verify(mGroup).addPreference(argumentCaptor.capture());
Preference preference = argumentCaptor.getValue(); Preference preference = argumentCaptor.getValue();
assertThat(preference.getSummary()).isEqualTo("0.03GB"); assertThat(preference.getSummary()).isEqualTo("0.03 GB");
} }
@Test @Test

View File

@@ -84,8 +84,6 @@ public class StorageItemPreferenceControllerTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
SettingsShadowResources.overrideResource("android:string/fileSizeSuffix", "%1$s %2$s");
SettingsShadowResources.overrideResource("android:string/gigabyteShort", "GB");
mContext = spy(RuntimeEnvironment.application.getApplicationContext()); mContext = spy(RuntimeEnvironment.application.getApplicationContext());
FakeFeatureFactory.setupForTest(mContext); FakeFeatureFactory.setupForTest(mContext);
mFakeFeatureFactory = (FakeFeatureFactory) FakeFeatureFactory.getFactory(mContext); mFakeFeatureFactory = (FakeFeatureFactory) FakeFeatureFactory.getFactory(mContext);
@@ -287,12 +285,12 @@ public class StorageItemPreferenceControllerTest {
results.put(0, result); results.put(0, result);
mController.onLoadFinished(results, 0); mController.onLoadFinished(results, 0);
assertThat(audio.getSummary().toString()).isEqualTo("0.14GB"); assertThat(audio.getSummary().toString()).isEqualTo("0.14 GB");
assertThat(image.getSummary().toString()).isEqualTo("0.35GB"); assertThat(image.getSummary().toString()).isEqualTo("0.35 GB");
assertThat(games.getSummary().toString()).isEqualTo("0.08GB"); assertThat(games.getSummary().toString()).isEqualTo("0.08 GB");
assertThat(movies.getSummary().toString()).isEqualTo("0.16GB"); assertThat(movies.getSummary().toString()).isEqualTo("0.16 GB");
assertThat(apps.getSummary().toString()).isEqualTo("0.09GB"); assertThat(apps.getSummary().toString()).isEqualTo("0.09 GB");
assertThat(files.getSummary().toString()).isEqualTo("0.05GB"); assertThat(files.getSummary().toString()).isEqualTo("0.05 GB");
} }
@Test @Test

View File

@@ -122,7 +122,7 @@ public class UserProfileControllerTest {
verify(mScreen).addPreference(argumentCaptor.capture()); verify(mScreen).addPreference(argumentCaptor.capture());
Preference preference = argumentCaptor.getValue(); Preference preference = argumentCaptor.getValue();
assertThat(preference.getSummary()).isEqualTo("0.10GB"); assertThat(preference.getSummary()).isEqualTo("0.10 GB");
} }
@Test @Test

View File

@@ -1,8 +1,8 @@
To build the tests you can use the following command at the root of your android source tree 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 via for example The test apk then needs to be installed onto your test device via for example
$ adb install -r out/target/product/shamu/data/app/SettingsUnitTests/SettingsUnitTests.apk $ adb install -r ${ANDROID_PRODUCT_OUT}/data/app/SettingsUnitTests/SettingsUnitTests.apk
To run all tests: To run all tests:
$ adb shell am instrument -w com.android.settings.tests.unit/android.support.test.runner.AndroidJUnitRunner $ adb shell am instrument -w com.android.settings.tests.unit/android.support.test.runner.AndroidJUnitRunner

View File

@@ -22,6 +22,7 @@ import static com.android.settings.utils.FileSizeFormatter.MEGABYTE_IN_BYTES;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.content.Context; import android.content.Context;
import android.icu.util.MeasureUnit;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest; import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
@@ -46,7 +47,7 @@ public class FileSizeFormatterTest {
FileSizeFormatter.formatFileSize( FileSizeFormatter.formatFileSize(
mContext, mContext,
0 /* size */, 0 /* size */,
com.android.internal.R.string.gigabyteShort, MeasureUnit.GIGABYTE,
GIGABYTE_IN_BYTES)) GIGABYTE_IN_BYTES))
.isEqualTo("0.00 GB"); .isEqualTo("0.00 GB");
} }
@@ -57,7 +58,7 @@ public class FileSizeFormatterTest {
FileSizeFormatter.formatFileSize( FileSizeFormatter.formatFileSize(
mContext, mContext,
MEGABYTE_IN_BYTES * 11 /* size */, MEGABYTE_IN_BYTES * 11 /* size */,
com.android.internal.R.string.gigabyteShort, MeasureUnit.GIGABYTE,
GIGABYTE_IN_BYTES)) GIGABYTE_IN_BYTES))
.isEqualTo("0.01 GB"); .isEqualTo("0.01 GB");
} }
@@ -68,7 +69,7 @@ public class FileSizeFormatterTest {
FileSizeFormatter.formatFileSize( FileSizeFormatter.formatFileSize(
mContext, mContext,
MEGABYTE_IN_BYTES * 155 /* size */, MEGABYTE_IN_BYTES * 155 /* size */,
com.android.internal.R.string.gigabyteShort, MeasureUnit.GIGABYTE,
GIGABYTE_IN_BYTES)) GIGABYTE_IN_BYTES))
.isEqualTo("0.16 GB"); .isEqualTo("0.16 GB");
} }
@@ -79,7 +80,7 @@ public class FileSizeFormatterTest {
FileSizeFormatter.formatFileSize( FileSizeFormatter.formatFileSize(
mContext, mContext,
MEGABYTE_IN_BYTES * 1551 /* size */, MEGABYTE_IN_BYTES * 1551 /* size */,
com.android.internal.R.string.gigabyteShort, MeasureUnit.GIGABYTE,
GIGABYTE_IN_BYTES)) GIGABYTE_IN_BYTES))
.isEqualTo("1.6 GB"); .isEqualTo("1.6 GB");
} }
@@ -91,7 +92,7 @@ public class FileSizeFormatterTest {
FileSizeFormatter.formatFileSize( FileSizeFormatter.formatFileSize(
mContext, mContext,
GIGABYTE_IN_BYTES * 15 + MEGABYTE_IN_BYTES * 50 /* size */, GIGABYTE_IN_BYTES * 15 + MEGABYTE_IN_BYTES * 50 /* size */,
com.android.internal.R.string.gigabyteShort, MeasureUnit.GIGABYTE,
GIGABYTE_IN_BYTES)) GIGABYTE_IN_BYTES))
.isEqualTo("15 GB"); .isEqualTo("15 GB");
} }
@@ -102,7 +103,7 @@ public class FileSizeFormatterTest {
FileSizeFormatter.formatFileSize( FileSizeFormatter.formatFileSize(
mContext, mContext,
MEGABYTE_IN_BYTES * -155 /* size */, MEGABYTE_IN_BYTES * -155 /* size */,
com.android.internal.R.string.gigabyteShort, MeasureUnit.GIGABYTE,
GIGABYTE_IN_BYTES)) GIGABYTE_IN_BYTES))
.isEqualTo("-0.16 GB"); .isEqualTo("-0.16 GB");
} }