diff --git a/libraries/datasource/src/androidTest/java/androidx/media3/datasource/DefaultHttpDataSourceContractTest.java b/libraries/datasource/src/androidTest/java/androidx/media3/datasource/DefaultHttpDataSourceContractTest.java index c9059fba42..7c9e750538 100644 --- a/libraries/datasource/src/androidTest/java/androidx/media3/datasource/DefaultHttpDataSourceContractTest.java +++ b/libraries/datasource/src/androidTest/java/androidx/media3/datasource/DefaultHttpDataSourceContractTest.java @@ -20,7 +20,6 @@ import androidx.media3.test.utils.DataSourceContractTest; import androidx.media3.test.utils.HttpDataSourceTestEnv; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; -import org.junit.Ignore; import org.junit.Rule; import org.junit.runner.RunWith; @@ -44,8 +43,4 @@ public class DefaultHttpDataSourceContractTest extends DataSourceContractTest { protected Uri getNotFoundUri() { return Uri.parse(httpDataSourceTestEnv.getNonexistentUrl()); } - - @Override - @Ignore("internal b/205811776") - public void getResponseHeaders_noNullKeysOrValues() {} } diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/DefaultHttpDataSource.java b/libraries/datasource/src/main/java/androidx/media3/datasource/DefaultHttpDataSource.java index d2337b2205..2e8be29c17 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/DefaultHttpDataSource.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/DefaultHttpDataSource.java @@ -30,6 +30,9 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSpec.HttpMethod; import com.google.common.base.Predicate; +import com.google.common.collect.ForwardingMap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; import com.google.common.net.HttpHeaders; import java.io.IOException; import java.io.InputStream; @@ -40,10 +43,10 @@ import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.NoRouteToHostException; import java.net.URL; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.zip.GZIPInputStream; /** @@ -312,7 +315,18 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou @Override public Map> getResponseHeaders() { - return connection == null ? Collections.emptyMap() : connection.getHeaderFields(); + if (connection == null) { + return ImmutableMap.of(); + } + // connection.getHeaderFields() always contains a null key with a value like + // ["HTTP/1.1 200 OK"]. The response code is available from HttpURLConnection#getResponseCode() + // and the HTTP version is fixed when establishing the connection. + // DataSource#getResponseHeaders() doesn't allow null keys in the returned map, so we need to + // remove it. + // connection.getHeaderFields() returns a special unmodifiable case-insensitive Map + // so we can't just remove the null key or make a copy without the null key. Instead we wrap it + // in a ForwardingMap subclass that ignores and filters out null keys in the read methods. + return new NullFilteringHeadersMap(connection.getHeaderFields()); } @Override @@ -817,4 +831,64 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou String contentEncoding = connection.getHeaderField("Content-Encoding"); return "gzip".equalsIgnoreCase(contentEncoding); } + + private static class NullFilteringHeadersMap extends ForwardingMap> { + + private final Map> headers; + + public NullFilteringHeadersMap(Map> headers) { + this.headers = headers; + } + + @Override + protected Map> delegate() { + return headers; + } + + @Override + public boolean containsKey(@Nullable Object key) { + return key != null && super.containsKey(key); + } + + @Nullable + @Override + public List get(@Nullable Object key) { + return key == null ? null : super.get(key); + } + + @Override + public Set keySet() { + return Sets.filter(super.keySet(), key -> key != null); + } + + @Override + public Set>> entrySet() { + return Sets.filter(super.entrySet(), entry -> entry.getKey() != null); + } + + @Override + public int size() { + return super.size() - (super.containsKey(null) ? 1 : 0); + } + + @Override + public boolean isEmpty() { + return super.isEmpty() || (super.size() == 1 && super.containsKey(null)); + } + + @Override + public boolean containsValue(@Nullable Object value) { + return super.standardContainsValue(value); + } + + @Override + public boolean equals(@Nullable Object object) { + return object != null && super.standardEquals(object); + } + + @Override + public int hashCode() { + return super.standardHashCode(); + } + } } diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/DataSourceContractTest.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/DataSourceContractTest.java index 0de48ee467..fd88b9261d 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/DataSourceContractTest.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/DataSourceContractTest.java @@ -547,11 +547,6 @@ public abstract class DataSourceContractTest { Map> responseHeaders = dataSource.getResponseHeaders(); for (String key : responseHeaders.keySet()) { - // TODO(internal b/205811776): Remove this when DefaultHttpDataSource is fixed to not - // return a null key. - if (key == null) { - continue; - } String caseFlippedKey = invertAsciiCaseOfEveryOtherCharacter(key); assertWithMessage("key='%s', caseFlippedKey='%s'", key, caseFlippedKey) .that(responseHeaders.get(caseFlippedKey))