Refactor answer callback for async DNS query JAVA API

1. refactor AnswerCallback with a generic type
2. support onError in AnswerCallback
3. Fix minor problem reported from API Review

Bug: 124882626
Test: built, flashed, booted
      atest DnsResolverTest DnsPacketTest

Change-Id: I685c9989f8401acb63d2e83f552b2d5b20c41af0
This commit is contained in:
Luke Huang
2019-03-04 17:08:03 +08:00
parent 122c557701
commit 6d72b2c073

View File

@@ -37,8 +37,6 @@ import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
/** /**
* Dns resolver class for asynchronous dns querying * Dns resolver class for asynchronous dns querying
@@ -81,66 +79,138 @@ public final class DnsResolver {
public static final int FLAG_NO_CACHE_STORE = 1 << 1; public static final int FLAG_NO_CACHE_STORE = 1 << 1;
public static final int FLAG_NO_CACHE_LOOKUP = 1 << 2; public static final int FLAG_NO_CACHE_LOOKUP = 1 << 2;
private static final int DNS_RAW_RESPONSE = 1;
private static final int NETID_UNSET = 0; private static final int NETID_UNSET = 0;
private static final DnsResolver sInstance = new DnsResolver(); private static final DnsResolver sInstance = new DnsResolver();
/**
* listener for receiving raw answers
*/
public interface RawAnswerListener {
/**
* {@code byte[]} is {@code null} if query timed out
*/
void onAnswer(@Nullable byte[] answer);
}
/**
* listener for receiving parsed answers
*/
public interface InetAddressAnswerListener {
/**
* Will be called exactly once with all the answers to the query.
* size of addresses will be zero if no available answer could be parsed.
*/
void onAnswer(@NonNull List<InetAddress> addresses);
}
/** /**
* Get instance for DnsResolver * Get instance for DnsResolver
*/ */
public static DnsResolver getInstance() { public static @NonNull DnsResolver getInstance() {
return sInstance; return sInstance;
} }
private DnsResolver() {} private DnsResolver() {}
/** /**
* Pass in a blob and corresponding setting, * Answer parser for parsing raw answers
* get a blob back asynchronously with the entire raw answer. *
* @param <T> The type of the parsed answer
*/
public interface AnswerParser<T> {
/**
* Creates a <T> answer by parsing the given raw answer.
*
* @param rawAnswer the raw answer to be parsed
* @return a parsed <T> answer
* @throws ParseException if parsing failed
*/
@NonNull T parse(@NonNull byte[] rawAnswer) throws ParseException;
}
/**
* Base class for answer callbacks
*
* @param <T> The type of the parsed answer
*/
public abstract static class AnswerCallback<T> {
/** @hide */
public final AnswerParser<T> parser;
public AnswerCallback(@NonNull AnswerParser<T> parser) {
this.parser = parser;
};
/**
* Success response to
* {@link android.net.DnsResolver#query query()}.
*
* Invoked when the answer to a query was successfully parsed.
*
* @param answer parsed answer to the query.
*
* {@see android.net.DnsResolver#query query()}
*/
public abstract void onAnswer(@NonNull T answer);
/**
* Error response to
* {@link android.net.DnsResolver#query query()}.
*
* Invoked when there is no valid answer to
* {@link android.net.DnsResolver#query query()}
*
* @param exception a {@link ParseException} object with additional
* detail regarding the failure
*/
public abstract void onParseException(@NonNull ParseException exception);
/**
* Error response to
* {@link android.net.DnsResolver#query query()}.
*
* Invoked if an error happens when
* issuing the DNS query or receiving the result.
* {@link android.net.DnsResolver#query query()}
*
* @param exception an {@link ErrnoException} object with additional detail
* regarding the failure
*/
public abstract void onQueryException(@NonNull ErrnoException exception);
}
/**
* Callback for receiving raw answers
*/
public abstract static class RawAnswerCallback extends AnswerCallback<byte[]> {
public RawAnswerCallback() {
super(rawAnswer -> rawAnswer);
}
}
/**
* Callback for receiving parsed {@link InetAddress} answers
*
* Note that if the answer does not contain any IP addresses,
* onAnswer will be called with an empty list.
*/
public abstract static class InetAddressAnswerCallback
extends AnswerCallback<List<InetAddress>> {
public InetAddressAnswerCallback() {
super(rawAnswer -> new DnsAddressAnswer(rawAnswer).getAddresses());
}
}
/**
* Pass in a blob and corresponding flags, get an answer back asynchronously
* through {@link AnswerCallback}.
* *
* @param network {@link Network} specifying which network for querying. * @param network {@link Network} specifying which network for querying.
* {@code null} for query on default network. * {@code null} for query on default network.
* @param query blob message * @param query blob message
* @param flags flags as a combination of the FLAGS_* constants * @param flags flags as a combination of the FLAGS_* constants
* @param handler {@link Handler} to specify the thread * @param handler {@link Handler} to specify the thread
* upon which the {@link RawAnswerListener} will be invoked. * upon which the {@link AnswerCallback} will be invoked.
* @param listener a {@link RawAnswerListener} which will be called to notify the caller * @param callback an {@link AnswerCallback} which will be called to notify the caller
* of the result of dns query. * of the result of dns query.
*/ */
public void query(@Nullable Network network, @NonNull byte[] query, @QueryFlag int flags, public <T> void query(@Nullable Network network, @NonNull byte[] query, @QueryFlag int flags,
@NonNull Handler handler, @NonNull RawAnswerListener listener) throws ErrnoException { @NonNull Handler handler, @NonNull AnswerCallback<T> callback) {
final FileDescriptor queryfd = resNetworkSend((network != null final FileDescriptor queryfd;
try {
queryfd = resNetworkSend((network != null
? network.netId : NETID_UNSET), query, query.length, flags); ? network.netId : NETID_UNSET), query, query.length, flags);
registerFDListener(handler.getLooper().getQueue(), queryfd, } catch (ErrnoException e) {
answerbuf -> listener.onAnswer(answerbuf)); callback.onQueryException(e);
return;
}
registerFDListener(handler.getLooper().getQueue(), queryfd, callback);
} }
/** /**
* Pass in a domain name and corresponding setting, * Pass in a domain name and corresponding setting, get an answer back asynchronously
* get a blob back asynchronously with the entire raw answer. * through {@link AnswerCallback}.
* *
* @param network {@link Network} specifying which network for querying. * @param network {@link Network} specifying which network for querying.
* {@code null} for query on default network. * {@code null} for query on default network.
@@ -149,52 +219,26 @@ public final class DnsResolver {
* @param nsType dns resource record (RR) type as one of the TYPE_* constants * @param nsType dns resource record (RR) type as one of the TYPE_* constants
* @param flags flags as a combination of the FLAGS_* constants * @param flags flags as a combination of the FLAGS_* constants
* @param handler {@link Handler} to specify the thread * @param handler {@link Handler} to specify the thread
* upon which the {@link RawAnswerListener} will be invoked. * upon which the {@link AnswerCallback} will be invoked.
* @param listener a {@link RawAnswerListener} which will be called to notify the caller * @param callback an {@link AnswerCallback} which will be called to notify the caller
* of the result of dns query. * of the result of dns query.
*/ */
public void query(@Nullable Network network, @NonNull String domain, @QueryClass int nsClass, public <T> void query(@Nullable Network network, @NonNull String domain,
@QueryType int nsType, @QueryFlag int flags, @QueryClass int nsClass, @QueryType int nsType, @QueryFlag int flags,
@NonNull Handler handler, @NonNull RawAnswerListener listener) throws ErrnoException { @NonNull Handler handler, @NonNull AnswerCallback<T> callback) {
final FileDescriptor queryfd = resNetworkQuery((network != null final FileDescriptor queryfd;
try {
queryfd = resNetworkQuery((network != null
? network.netId : NETID_UNSET), domain, nsClass, nsType, flags); ? network.netId : NETID_UNSET), domain, nsClass, nsType, flags);
registerFDListener(handler.getLooper().getQueue(), queryfd, } catch (ErrnoException e) {
answerbuf -> listener.onAnswer(answerbuf)); callback.onQueryException(e);
return;
}
registerFDListener(handler.getLooper().getQueue(), queryfd, callback);
} }
/** private <T> void registerFDListener(@NonNull MessageQueue queue,
* Pass in a domain name and corresponding setting, @NonNull FileDescriptor queryfd, @NonNull AnswerCallback<T> answerCallback) {
* get back a set of InetAddresses asynchronously.
*
* @param network {@link Network} specifying which network for querying.
* {@code null} for query on default network.
* @param domain domain name for querying
* @param flags flags as a combination of the FLAGS_* constants
* @param handler {@link Handler} to specify the thread
* upon which the {@link InetAddressAnswerListener} will be invoked.
* @param listener an {@link InetAddressAnswerListener} which will be called to
* notify the caller of the result of dns query.
*
*/
public void query(@Nullable Network network, @NonNull String domain, @QueryFlag int flags,
@NonNull Handler handler, @NonNull InetAddressAnswerListener listener)
throws ErrnoException {
final FileDescriptor v4fd = resNetworkQuery((network != null
? network.netId : NETID_UNSET), domain, CLASS_IN, TYPE_A, flags);
final FileDescriptor v6fd = resNetworkQuery((network != null
? network.netId : NETID_UNSET), domain, CLASS_IN, TYPE_AAAA, flags);
final InetAddressAnswerAccumulator accmulator =
new InetAddressAnswerAccumulator(2, listener);
final Consumer<byte[]> consumer = answerbuf ->
accmulator.accumulate(parseAnswers(answerbuf));
registerFDListener(handler.getLooper().getQueue(), v4fd, consumer);
registerFDListener(handler.getLooper().getQueue(), v6fd, consumer);
}
private void registerFDListener(@NonNull MessageQueue queue,
@NonNull FileDescriptor queryfd, @NonNull Consumer<byte[]> answerConsumer) {
queue.addOnFileDescriptorEventListener( queue.addOnFileDescriptorEventListener(
queryfd, queryfd,
FD_EVENTS, FD_EVENTS,
@@ -207,15 +251,22 @@ public final class DnsResolver {
answerbuf = resNetworkResult(fd); answerbuf = resNetworkResult(fd);
} catch (ErrnoException e) { } catch (ErrnoException e) {
Log.e(TAG, "resNetworkResult:" + e.toString()); Log.e(TAG, "resNetworkResult:" + e.toString());
answerCallback.onQueryException(e);
return 0;
}
try {
answerCallback.onAnswer(answerCallback.parser.parse(answerbuf));
} catch (ParseException e) {
answerCallback.onParseException(e);
} }
answerConsumer.accept(answerbuf);
// Unregister this fd listener // Unregister this fd listener
return 0; return 0;
}); });
} }
private class DnsAddressAnswer extends DnsPacket { private static class DnsAddressAnswer extends DnsPacket {
private static final String TAG = "DnsResolver.DnsAddressAnswer"; private static final String TAG = "DnsResolver.DnsAddressAnswer";
private static final boolean DBG = false; private static final boolean DBG = false;
@@ -226,12 +277,6 @@ public final class DnsResolver {
if ((mHeader.flags & (1 << 15)) == 0) { if ((mHeader.flags & (1 << 15)) == 0) {
throw new ParseException("Not an answer packet"); throw new ParseException("Not an answer packet");
} }
if (mHeader.rcode != 0) {
throw new ParseException("Response error, rcode:" + mHeader.rcode);
}
if (mHeader.getRecordCount(ANSECTION) == 0) {
throw new ParseException("No available answer");
}
if (mHeader.getRecordCount(QDSECTION) == 0) { if (mHeader.getRecordCount(QDSECTION) == 0) {
throw new ParseException("No question found"); throw new ParseException("No question found");
} }
@@ -241,6 +286,8 @@ public final class DnsResolver {
public @NonNull List<InetAddress> getAddresses() { public @NonNull List<InetAddress> getAddresses() {
final List<InetAddress> results = new ArrayList<InetAddress>(); final List<InetAddress> results = new ArrayList<InetAddress>();
if (mHeader.getRecordCount(ANSECTION) == 0) return results;
for (final DnsRecord ansSec : mRecords[ANSECTION]) { for (final DnsRecord ansSec : mRecords[ANSECTION]) {
// Only support A and AAAA, also ignore answers if query type != answer type. // Only support A and AAAA, also ignore answers if query type != answer type.
int nsType = ansSec.nsType; int nsType = ansSec.nsType;
@@ -259,34 +306,4 @@ public final class DnsResolver {
} }
} }
private @Nullable List<InetAddress> parseAnswers(@Nullable byte[] data) {
try {
return (data == null) ? null : new DnsAddressAnswer(data).getAddresses();
} catch (DnsPacket.ParseException e) {
Log.e(TAG, "Parse answer fail " + e.getMessage());
return null;
}
}
private class InetAddressAnswerAccumulator {
private final List<InetAddress> mAllAnswers;
private final InetAddressAnswerListener mAnswerListener;
private final int mTargetAnswerCount;
private int mReceivedAnswerCount = 0;
InetAddressAnswerAccumulator(int size, @NonNull InetAddressAnswerListener listener) {
mTargetAnswerCount = size;
mAllAnswers = new ArrayList<>();
mAnswerListener = listener;
}
public void accumulate(@Nullable List<InetAddress> answer) {
if (null != answer) {
mAllAnswers.addAll(answer);
}
if (++mReceivedAnswerCount == mTargetAnswerCount) {
mAnswerListener.onAnswer(mAllAnswers);
}
}
}
} }