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:
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
|
||||||
|
@@ -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);
|
||||||
|
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
@@ -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
|
||||||
|
@@ -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");
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user