From 70c70530bd6793869736ec894498e4ebf5dc9b20 Mon Sep 17 00:00:00 2001 From: Jeff Sharkey Date: Wed, 16 May 2012 14:51:19 -0700 Subject: [PATCH] Transition from DEV network stats to XT. When XT stats are available, transition to prefer them over DEV, since they aren't subject to hardware driver bugs. Only switches at the first atomic XT bucket, and adds a Settings.Secure flag to force back to DEV if needed. Includes tests to cover transition. Fix tests where device overlay would change which network types reflected data usage. Test both history and summary APIs. Fixed collection timestamps to reflect full buckets. Bug: 6504744 Change-Id: Idd7f3b2fdb064c36547c85c51c214fd938c59b7e --- core/java/android/net/NetworkStats.java | 8 ++ .../java/android/net/NetworkStatsHistory.java | 18 ++- core/java/android/net/NetworkTemplate.java | 11 +- core/java/android/provider/Settings.java | 2 + .../server/net/NetworkStatsCollection.java | 33 +++++- .../server/net/NetworkStatsService.java | 61 +++++++++- .../server/NetworkStatsServiceTest.java | 104 +++++++++++++++++- .../net/NetworkStatsCollectionTest.java | 26 +++++ 8 files changed, 248 insertions(+), 15 deletions(-) diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java index 844d05564b2cb..fb7a4f8ad216b 100644 --- a/core/java/android/net/NetworkStats.java +++ b/core/java/android/net/NetworkStats.java @@ -111,6 +111,14 @@ public class NetworkStats implements Parcelable { && operations == 0; } + public void add(Entry another) { + this.rxBytes += another.rxBytes; + this.rxPackets += another.rxPackets; + this.txBytes += another.txBytes; + this.txPackets += another.txPackets; + this.operations += another.operations; + } + @Override public String toString() { final StringBuilder builder = new StringBuilder(); diff --git a/core/java/android/net/NetworkStatsHistory.java b/core/java/android/net/NetworkStatsHistory.java index 0003c6e3d6ae0..a37c26f90d73e 100644 --- a/core/java/android/net/NetworkStatsHistory.java +++ b/core/java/android/net/NetworkStatsHistory.java @@ -342,11 +342,23 @@ public class NetworkStatsHistory implements Parcelable { * for combining together stats for external reporting. */ public void recordEntireHistory(NetworkStatsHistory input) { + recordHistory(input, Long.MIN_VALUE, Long.MAX_VALUE); + } + + /** + * Record given {@link NetworkStatsHistory} into this history, copying only + * buckets that atomically occur in the inclusive time range. Doesn't + * interpolate across partial buckets. + */ + public void recordHistory(NetworkStatsHistory input, long start, long end) { final NetworkStats.Entry entry = new NetworkStats.Entry( IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0L); for (int i = 0; i < input.bucketCount; i++) { - final long start = input.bucketStart[i]; - final long end = start + input.bucketDuration; + final long bucketStart = input.bucketStart[i]; + final long bucketEnd = bucketStart + input.bucketDuration; + + // skip when bucket is outside requested range + if (bucketStart < start || bucketEnd > end) continue; entry.rxBytes = getLong(input.rxBytes, i, 0L); entry.rxPackets = getLong(input.rxPackets, i, 0L); @@ -354,7 +366,7 @@ public class NetworkStatsHistory implements Parcelable { entry.txPackets = getLong(input.txPackets, i, 0L); entry.operations = getLong(input.operations, i, 0L); - recordData(start, end, entry); + recordData(bucketStart, bucketEnd, entry); } } diff --git a/core/java/android/net/NetworkTemplate.java b/core/java/android/net/NetworkTemplate.java index 39a4d7bb0d8f4..d8e53d523f074 100644 --- a/core/java/android/net/NetworkTemplate.java +++ b/core/java/android/net/NetworkTemplate.java @@ -61,6 +61,13 @@ public class NetworkTemplate implements Parcelable { com.android.internal.R.array.config_data_usage_network_types); } + private static boolean sForceAllNetworkTypes = false; + + // @VisibleForTesting + public static void forceAllNetworkTypes() { + sForceAllNetworkTypes = true; + } + /** * Template to match {@link ConnectivityManager#TYPE_MOBILE} networks with * the given IMSI. @@ -225,7 +232,7 @@ public class NetworkTemplate implements Parcelable { // TODO: consider matching against WiMAX subscriber identity return true; } else { - return (contains(DATA_USAGE_NETWORK_TYPES, ident.mType) + return ((sForceAllNetworkTypes || contains(DATA_USAGE_NETWORK_TYPES, ident.mType)) && Objects.equal(mSubscriberId, ident.mSubscriberId)); } } @@ -291,7 +298,7 @@ public class NetworkTemplate implements Parcelable { if (ident.mType == TYPE_WIMAX) { return true; } else { - return contains(DATA_USAGE_NETWORK_TYPES, ident.mType); + return sForceAllNetworkTypes || contains(DATA_USAGE_NETWORK_TYPES, ident.mType); } } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ea3cab44477c7..a0641d5db1b61 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -4210,6 +4210,8 @@ public final class Settings { public static final String NETSTATS_GLOBAL_ALERT_BYTES = "netstats_global_alert_bytes"; /** {@hide} */ public static final String NETSTATS_SAMPLE_ENABLED = "netstats_sample_enabled"; + /** {@hide} */ + public static final String NETSTATS_REPORT_XT_OVER_DEV = "netstats_report_xt_over_dev"; /** {@hide} */ public static final String NETSTATS_DEV_BUCKET_DURATION = "netstats_dev_bucket_duration"; diff --git a/services/java/com/android/server/net/NetworkStatsCollection.java b/services/java/com/android/server/net/NetworkStatsCollection.java index c2e475a5b4ead..9ddf011069147 100644 --- a/services/java/com/android/server/net/NetworkStatsCollection.java +++ b/services/java/com/android/server/net/NetworkStatsCollection.java @@ -71,7 +71,7 @@ public class NetworkStatsCollection implements FileRotator.Reader { private HashMap mStats = Maps.newHashMap(); - private long mBucketDuration; + private final long mBucketDuration; private long mStartMillis; private long mEndMillis; @@ -95,6 +95,18 @@ public class NetworkStatsCollection implements FileRotator.Reader { return mStartMillis; } + /** + * Return first atomic bucket in this collection, which is more conservative + * than {@link #mStartMillis}. + */ + public long getFirstAtomicBucketMillis() { + if (mStartMillis == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } else { + return mStartMillis + mBucketDuration; + } + } + public long getEndMillis() { return mEndMillis; } @@ -121,6 +133,15 @@ public class NetworkStatsCollection implements FileRotator.Reader { */ public NetworkStatsHistory getHistory( NetworkTemplate template, int uid, int set, int tag, int fields) { + return getHistory(template, uid, set, tag, fields, Long.MIN_VALUE, Long.MAX_VALUE); + } + + /** + * Combine all {@link NetworkStatsHistory} in this collection which match + * the requested parameters. + */ + public NetworkStatsHistory getHistory( + NetworkTemplate template, int uid, int set, int tag, int fields, long start, long end) { final NetworkStatsHistory combined = new NetworkStatsHistory( mBucketDuration, estimateBuckets(), fields); for (Map.Entry entry : mStats.entrySet()) { @@ -128,7 +149,7 @@ public class NetworkStatsCollection implements FileRotator.Reader { final boolean setMatches = set == SET_ALL || key.set == set; if (key.uid == uid && setMatches && key.tag == tag && templateMatches(template, key.ident)) { - combined.recordEntireHistory(entry.getValue()); + combined.recordHistory(entry.getValue(), start, end); } } return combined; @@ -145,6 +166,9 @@ public class NetworkStatsCollection implements FileRotator.Reader { final NetworkStats.Entry entry = new NetworkStats.Entry(); NetworkStatsHistory.Entry historyEntry = null; + // shortcut when we know stats will be empty + if (start == end) return stats; + for (Map.Entry mapEntry : mStats.entrySet()) { final Key key = mapEntry.getKey(); if (templateMatches(template, key.ident)) { @@ -175,8 +199,9 @@ public class NetworkStatsCollection implements FileRotator.Reader { */ public void recordData(NetworkIdentitySet ident, int uid, int set, int tag, long start, long end, NetworkStats.Entry entry) { - noteRecordedHistory(start, end, entry.rxBytes + entry.txBytes); - findOrCreateHistory(ident, uid, set, tag).recordData(start, end, entry); + final NetworkStatsHistory history = findOrCreateHistory(ident, uid, set, tag); + history.recordData(start, end, entry); + noteRecordedHistory(history.getStart(), history.getEnd(), entry.rxBytes + entry.txBytes); } /** diff --git a/services/java/com/android/server/net/NetworkStatsService.java b/services/java/com/android/server/net/NetworkStatsService.java index 0e93b0affc6b6..ba122ecdf2317 100644 --- a/services/java/com/android/server/net/NetworkStatsService.java +++ b/services/java/com/android/server/net/NetworkStatsService.java @@ -44,6 +44,7 @@ import static android.provider.Settings.Secure.NETSTATS_DEV_PERSIST_BYTES; import static android.provider.Settings.Secure.NETSTATS_DEV_ROTATE_AGE; import static android.provider.Settings.Secure.NETSTATS_GLOBAL_ALERT_BYTES; import static android.provider.Settings.Secure.NETSTATS_POLL_INTERVAL; +import static android.provider.Settings.Secure.NETSTATS_REPORT_XT_OVER_DEV; import static android.provider.Settings.Secure.NETSTATS_SAMPLE_ENABLED; import static android.provider.Settings.Secure.NETSTATS_TIME_CACHE_MAX_AGE; import static android.provider.Settings.Secure.NETSTATS_UID_BUCKET_DURATION; @@ -177,6 +178,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub { public long getPollInterval(); public long getTimeCacheMaxAge(); public boolean getSampleEnabled(); + public boolean getReportXtOverDev(); public static class Config { public final long bucketDuration; @@ -221,6 +223,8 @@ public class NetworkStatsService extends INetworkStatsService.Stub { /** Cached {@link #mDevRecorder} stats. */ private NetworkStatsCollection mDevStatsCached; + /** Cached {@link #mXtRecorder} stats. */ + private NetworkStatsCollection mXtStatsCached; /** Current counter sets for each UID. */ private SparseIntArray mActiveUidCounterSet = new SparseIntArray(); @@ -295,6 +299,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub { // read historical network stats from disk, since policy service // might need them right away. mDevStatsCached = mDevRecorder.getOrLoadCompleteLocked(); + mXtStatsCached = mXtRecorder.getOrLoadCompleteLocked(); // bootstrap initial stats to prevent double-counting later bootstrapStatsLocked(); @@ -371,6 +376,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub { mUidTagRecorder = null; mDevStatsCached = null; + mXtStatsCached = null; mSystemReady = false; } @@ -469,12 +475,12 @@ public class NetworkStatsService extends INetworkStatsService.Stub { @Override public NetworkStats getSummaryForNetwork( NetworkTemplate template, long start, long end) { - return mDevStatsCached.getSummary(template, start, end); + return internalGetSummaryForNetwork(template, start, end); } @Override public NetworkStatsHistory getHistoryForNetwork(NetworkTemplate template, int fields) { - return mDevStatsCached.getHistory(template, UID_ALL, SET_ALL, TAG_NONE, fields); + return internalGetHistoryForNetwork(template, fields); } @Override @@ -507,11 +513,56 @@ public class NetworkStatsService extends INetworkStatsService.Stub { }; } + /** + * Return network summary, splicing between {@link #mDevStatsCached} + * and {@link #mXtStatsCached} when appropriate. + */ + private NetworkStats internalGetSummaryForNetwork( + NetworkTemplate template, long start, long end) { + if (!mSettings.getReportXtOverDev()) { + // shortcut when XT reporting disabled + return mDevStatsCached.getSummary(template, start, end); + } + + // splice stats between DEV and XT, switching over from DEV to XT at + // first atomic bucket. + final long firstAtomicBucket = mXtStatsCached.getFirstAtomicBucketMillis(); + final NetworkStats dev = mDevStatsCached.getSummary( + template, Math.min(start, firstAtomicBucket), Math.min(end, firstAtomicBucket)); + final NetworkStats xt = mXtStatsCached.getSummary( + template, Math.max(start, firstAtomicBucket), Math.max(end, firstAtomicBucket)); + + xt.combineAllValues(dev); + return xt; + } + + /** + * Return network history, splicing between {@link #mDevStatsCached} + * and {@link #mXtStatsCached} when appropriate. + */ + private NetworkStatsHistory internalGetHistoryForNetwork(NetworkTemplate template, int fields) { + if (!mSettings.getReportXtOverDev()) { + // shortcut when XT reporting disabled + return mDevStatsCached.getHistory(template, UID_ALL, SET_ALL, TAG_NONE, fields); + } + + // splice stats between DEV and XT, switching over from DEV to XT at + // first atomic bucket. + final long firstAtomicBucket = mXtStatsCached.getFirstAtomicBucketMillis(); + final NetworkStatsHistory dev = mDevStatsCached.getHistory( + template, UID_ALL, SET_ALL, TAG_NONE, fields, Long.MIN_VALUE, firstAtomicBucket); + final NetworkStatsHistory xt = mXtStatsCached.getHistory( + template, UID_ALL, SET_ALL, TAG_NONE, fields, firstAtomicBucket, Long.MAX_VALUE); + + xt.recordEntireHistory(dev); + return xt; + } + @Override public long getNetworkTotalBytes(NetworkTemplate template, long start, long end) { mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG); assertBandwidthControlEnabled(); - return mDevStatsCached.getSummary(template, start, end).getTotalBytes(); + return internalGetSummaryForNetwork(template, start, end).getTotalBytes(); } @Override @@ -1190,6 +1241,10 @@ public class NetworkStatsService extends INetworkStatsService.Stub { return getSecureBoolean(NETSTATS_SAMPLE_ENABLED, true); } @Override + public boolean getReportXtOverDev() { + return getSecureBoolean(NETSTATS_REPORT_XT_OVER_DEV, true); + } + @Override public Config getDevConfig() { return new Config(getSecureLong(NETSTATS_DEV_BUCKET_DURATION, HOUR_IN_MILLIS), getSecureLong(NETSTATS_DEV_ROTATE_AGE, 15 * DAY_IN_MILLIS), diff --git a/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java index 332d1980167f7..cdc4d784cf01c 100644 --- a/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java @@ -777,10 +777,80 @@ public class NetworkStatsServiceTest extends AndroidTestCase { } + public void testReportXtOverDev() throws Exception { + // bring mobile network online + expectCurrentTime(); + expectDefaultSettings(); + expectNetworkState(buildMobile3gState(IMSI_1)); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + expectNetworkStatsPoll(); + + replay(); + mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION_IMMEDIATE)); + verifyAndReset(); + + // create some traffic, but only for DEV, and across 1.5 buckets + incrementCurrentTime(90 * MINUTE_IN_MILLIS); + expectCurrentTime(); + expectDefaultSettings(); + expectNetworkStatsSummaryDev(new NetworkStats(getElapsedRealtime(), 1) + .addIfaceValues(TEST_IFACE, 6000L, 60L, 3000L, 30L)); + expectNetworkStatsSummaryXt(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + expectNetworkStatsPoll(); + + replay(); + mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL)); + + // verify service recorded history: + // 4000(dev) + 2000(dev) + assertNetworkTotal(sTemplateImsi1, 6000L, 60L, 3000L, 30L, 0); + verifyAndReset(); + + // create traffic on both DEV and XT, across two buckets + incrementCurrentTime(2 * HOUR_IN_MILLIS); + expectCurrentTime(); + expectDefaultSettings(); + expectNetworkStatsSummaryDev(new NetworkStats(getElapsedRealtime(), 1) + .addIfaceValues(TEST_IFACE, 6004L, 64L, 3004L, 34L)); + expectNetworkStatsSummaryXt(new NetworkStats(getElapsedRealtime(), 1) + .addIfaceValues(TEST_IFACE, 10240L, 0L, 0L, 0L)); + expectNetworkStatsUidDetail(buildEmptyStats()); + expectNetworkStatsPoll(); + + replay(); + mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL)); + + // verify that we switching reporting at the first atomic XT bucket, + // which should give us: + // 4000(dev) + 2000(dev) + 1(dev) + 5120(xt) + 2560(xt) + assertNetworkTotal(sTemplateImsi1, 13681L, 61L, 3001L, 31L, 0); + + // also test pure-DEV and pure-XT ranges + assertNetworkTotal(sTemplateImsi1, startTimeMillis(), + startTimeMillis() + 2 * HOUR_IN_MILLIS, 6001L, 61L, 3001L, 31L, 0); + assertNetworkTotal(sTemplateImsi1, startTimeMillis() + 2 * HOUR_IN_MILLIS, + startTimeMillis() + 4 * HOUR_IN_MILLIS, 7680L, 0L, 0L, 0L, 0); + + verifyAndReset(); + } + private void assertNetworkTotal(NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets, int operations) throws Exception { + assertNetworkTotal(template, Long.MIN_VALUE, Long.MAX_VALUE, rxBytes, rxPackets, txBytes, + txPackets, operations); + } + + private void assertNetworkTotal(NetworkTemplate template, long start, long end, long rxBytes, + long rxPackets, long txBytes, long txPackets, int operations) throws Exception { + // verify history API final NetworkStatsHistory history = mSession.getHistoryForNetwork(template, FIELD_ALL); - assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, rxBytes, rxPackets, txBytes, + assertValues(history, start, end, rxBytes, rxPackets, txBytes, txPackets, operations); + + // verify summary API + final NetworkStats stats = mSession.getSummaryForNetwork(template, start, end); + assertValues(stats, IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, rxPackets, txBytes, txPackets, operations); } @@ -791,10 +861,17 @@ public class NetworkStatsServiceTest extends AndroidTestCase { private void assertUidTotal(NetworkTemplate template, int uid, int set, long rxBytes, long rxPackets, long txBytes, long txPackets, int operations) throws Exception { + // verify history API final NetworkStatsHistory history = mSession.getHistoryForUid( template, uid, set, TAG_NONE, FIELD_ALL); assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, rxBytes, rxPackets, txBytes, txPackets, operations); + + // verify summary API + final NetworkStats stats = mSession.getSummaryForAllUid( + template, Long.MIN_VALUE, Long.MAX_VALUE, false); + assertValues(stats, IFACE_ALL, uid, set, TAG_NONE, rxBytes, rxPackets, txBytes, txPackets, + operations); } private void expectSystemReady() throws Exception { @@ -819,7 +896,15 @@ public class NetworkStatsServiceTest extends AndroidTestCase { } private void expectNetworkStatsSummary(NetworkStats summary) throws Exception { + expectNetworkStatsSummaryDev(summary); + expectNetworkStatsSummaryXt(summary); + } + + private void expectNetworkStatsSummaryDev(NetworkStats summary) throws Exception { expect(mNetManager.getNetworkStatsSummaryDev()).andReturn(summary).atLeastOnce(); + } + + private void expectNetworkStatsSummaryXt(NetworkStats summary) throws Exception { expect(mNetManager.getNetworkStatsSummaryXt()).andReturn(summary).atLeastOnce(); } @@ -847,6 +932,7 @@ public class NetworkStatsServiceTest extends AndroidTestCase { expect(mSettings.getPollInterval()).andReturn(HOUR_IN_MILLIS).anyTimes(); expect(mSettings.getTimeCacheMaxAge()).andReturn(DAY_IN_MILLIS).anyTimes(); expect(mSettings.getSampleEnabled()).andReturn(true).anyTimes(); + expect(mSettings.getReportXtOverDev()).andReturn(true).anyTimes(); final Config config = new Config(bucketDuration, deleteAge, deleteAge); expect(mSettings.getDevConfig()).andReturn(config).anyTimes(); @@ -885,8 +971,20 @@ public class NetworkStatsServiceTest extends AndroidTestCase { private static void assertValues(NetworkStats stats, String iface, int uid, int set, int tag, long rxBytes, long rxPackets, long txBytes, long txPackets, int operations) { - final int i = stats.findIndex(iface, uid, set, tag); - final NetworkStats.Entry entry = stats.getValues(i, null); + final NetworkStats.Entry entry = new NetworkStats.Entry(); + if (set == SET_DEFAULT || set == SET_ALL) { + final int i = stats.findIndex(iface, uid, SET_DEFAULT, tag); + if (i != -1) { + entry.add(stats.getValues(i, null)); + } + } + if (set == SET_FOREGROUND || set == SET_ALL) { + final int i = stats.findIndex(iface, uid, SET_FOREGROUND, tag); + if (i != -1) { + entry.add(stats.getValues(i, null)); + } + } + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets); assertEquals("unexpected txBytes", txBytes, entry.txBytes); diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java index 863482156e87e..1a6c289316887 100644 --- a/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java +++ b/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java @@ -16,7 +16,11 @@ package com.android.server.net; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; import static android.net.NetworkTemplate.buildTemplateMobileAll; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import android.content.res.Resources; @@ -47,6 +51,14 @@ public class NetworkStatsCollectionTest extends AndroidTestCase { private static final String TEST_FILE = "test.bin"; private static final String TEST_IMSI = "310260000000000"; + @Override + public void setUp() throws Exception { + super.setUp(); + + // ignore any device overlay while testing + NetworkTemplate.forceAllNetworkTypes(); + } + public void testReadLegacyNetwork() throws Exception { final File testFile = new File(getContext().getFilesDir(), TEST_FILE); stageFile(R.raw.netstats_v1, testFile); @@ -125,6 +137,20 @@ public class NetworkStatsCollectionTest extends AndroidTestCase { 77017831L, 100995L, 35436758L, 92344L); } + public void testStartEndAtomicBuckets() throws Exception { + final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS); + + // record empty data straddling between buckets + final NetworkStats.Entry entry = new NetworkStats.Entry(); + entry.rxBytes = 32; + collection.recordData(null, UID_ALL, SET_DEFAULT, TAG_NONE, 30 * MINUTE_IN_MILLIS, + 90 * MINUTE_IN_MILLIS, entry); + + // assert that we report boundary in atomic buckets + assertEquals(0, collection.getStartMillis()); + assertEquals(2 * HOUR_IN_MILLIS, collection.getEndMillis()); + } + /** * Copy a {@link Resources#openRawResource(int)} into {@link File} for * testing purposes.