blob: 997dcbbf308f19d5849ee750d30572f6a876c8f8 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity.mdns;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.LinkAddress;
import android.net.nsd.NsdServiceInfo;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.connectivity.mdns.MdnsPacketRepeater.PacketRepeaterCallback;
import java.io.IOException;
import java.util.List;
/**
* A class that handles advertising services on a {@link MdnsInterfaceSocket} tied to an interface.
*/
public class MdnsInterfaceAdvertiser {
private static final boolean DBG = MdnsAdvertiser.DBG;
@VisibleForTesting
public static final long EXIT_ANNOUNCEMENT_DELAY_MS = 100L;
@NonNull
private final String mTag;
@NonNull
private final ProbingCallback mProbingCallback = new ProbingCallback();
@NonNull
private final AnnouncingCallback mAnnouncingCallback = new AnnouncingCallback();
@NonNull
private final MdnsRecordRepository mRecordRepository;
@NonNull
private final Callback mCb;
// Callbacks are on the same looper thread, but posted to the next handler loop
@NonNull
private final Handler mCbHandler;
@NonNull
private final MdnsInterfaceSocket mSocket;
@NonNull
private final MdnsAnnouncer mAnnouncer;
@NonNull
private final MdnsProber mProber;
@NonNull
private final MdnsReplySender mReplySender;
/**
* Callbacks called by {@link MdnsInterfaceAdvertiser} to report status updates.
*/
interface Callback {
/**
* Called by the advertiser after it successfully registered a service, after probing.
*/
void onRegisterServiceSucceeded(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId);
/**
* Called by the advertiser when a conflict was found, during or after probing.
*
* If a conflict is found during probing, the {@link #renameServiceForConflict} must be
* called to restart probing and attempt registration with a different name.
*/
void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId);
/**
* Called by the advertiser when it destroyed itself.
*
* This can happen after a call to {@link #destroyNow()}, or after all services were
* unregistered and the advertiser finished sending exit announcements.
*/
void onDestroyed(@NonNull MdnsInterfaceSocket socket);
}
/**
* Callbacks from {@link MdnsProber}.
*/
private class ProbingCallback implements
PacketRepeaterCallback<MdnsProber.ProbingInfo> {
@Override
public void onFinished(MdnsProber.ProbingInfo info) {
final MdnsAnnouncer.AnnouncementInfo announcementInfo;
if (DBG) {
Log.v(mTag, "Probing finished for service " + info.getServiceId());
}
mCbHandler.post(() -> mCb.onRegisterServiceSucceeded(
MdnsInterfaceAdvertiser.this, info.getServiceId()));
try {
announcementInfo = mRecordRepository.onProbingSucceeded(info);
} catch (IOException e) {
Log.e(mTag, "Error building announcements", e);
return;
}
mAnnouncer.startSending(info.getServiceId(), announcementInfo,
0L /* initialDelayMs */);
}
}
/**
* Callbacks from {@link MdnsAnnouncer}.
*/
private class AnnouncingCallback
implements PacketRepeaterCallback<MdnsAnnouncer.AnnouncementInfo> {
// TODO: implement
}
/**
* Dependencies for {@link MdnsInterfaceAdvertiser}, useful for testing.
*/
@VisibleForTesting
public static class Dependencies {
/** @see MdnsRecordRepository */
@NonNull
public MdnsRecordRepository makeRecordRepository(@NonNull Looper looper) {
return new MdnsRecordRepository(looper);
}
/** @see MdnsReplySender */
@NonNull
public MdnsReplySender makeReplySender(@NonNull Looper looper,
@NonNull MdnsInterfaceSocket socket, @NonNull byte[] packetCreationBuffer) {
return new MdnsReplySender(looper, socket, packetCreationBuffer);
}
/** @see MdnsAnnouncer */
public MdnsAnnouncer makeMdnsAnnouncer(@NonNull String interfaceTag, @NonNull Looper looper,
@NonNull MdnsReplySender replySender,
@Nullable PacketRepeaterCallback<MdnsAnnouncer.AnnouncementInfo> cb) {
return new MdnsAnnouncer(interfaceTag, looper, replySender, cb);
}
/** @see MdnsProber */
public MdnsProber makeMdnsProber(@NonNull String interfaceTag, @NonNull Looper looper,
@NonNull MdnsReplySender replySender,
@NonNull PacketRepeaterCallback<MdnsProber.ProbingInfo> cb) {
return new MdnsProber(interfaceTag, looper, replySender, cb);
}
}
public MdnsInterfaceAdvertiser(@NonNull String logTag,
@NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> initialAddresses,
@NonNull Looper looper, @NonNull byte[] packetCreationBuffer, @NonNull Callback cb) {
this(logTag, socket, initialAddresses, looper, packetCreationBuffer, cb,
new Dependencies());
}
public MdnsInterfaceAdvertiser(@NonNull String logTag,
@NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> initialAddresses,
@NonNull Looper looper, @NonNull byte[] packetCreationBuffer, @NonNull Callback cb,
@NonNull Dependencies deps) {
mTag = MdnsInterfaceAdvertiser.class.getSimpleName() + "/" + logTag;
mRecordRepository = deps.makeRecordRepository(looper);
mRecordRepository.updateAddresses(initialAddresses);
mSocket = socket;
mCb = cb;
mCbHandler = new Handler(looper);
mReplySender = deps.makeReplySender(looper, socket, packetCreationBuffer);
mAnnouncer = deps.makeMdnsAnnouncer(logTag, looper, mReplySender,
mAnnouncingCallback);
mProber = deps.makeMdnsProber(logTag, looper, mReplySender, mProbingCallback);
}
/**
* Start the advertiser.
*
* The advertiser will stop itself when all services are removed and exit announcements sent,
* notifying via {@link Callback#onDestroyed}. This can also be triggered manually via
* {@link #destroyNow()}.
*/
public void start() {
// TODO: start receiving packets
}
/**
* Start advertising a service.
*
* @throws NameConflictException There is already a service being advertised with that name.
*/
public void addService(int id, NsdServiceInfo service) throws NameConflictException {
final int replacedExitingService = mRecordRepository.addService(id, service);
// Cancel announcements for the existing service. This only happens for exiting services
// (so cancelling exiting announcements), as per RecordRepository.addService.
if (replacedExitingService >= 0) {
if (DBG) {
Log.d(mTag, "Service " + replacedExitingService
+ " getting re-added, cancelling exit announcements");
}
mAnnouncer.stop(replacedExitingService);
}
mProber.startProbing(mRecordRepository.setServiceProbing(id));
}
/**
* Stop advertising a service.
*
* This will trigger exit announcements for the service.
*/
public void removeService(int id) {
mProber.stop(id);
mAnnouncer.stop(id);
final MdnsAnnouncer.AnnouncementInfo exitInfo = mRecordRepository.exitService(id);
if (exitInfo != null) {
// This effectively schedules destroyNow(), as it is to be called when the exit
// announcement finishes if there is no service left.
// A non-zero exit announcement delay follows legacy mdnsresponder behavior, and is
// also useful to ensure that when a host receives the exit announcement, the service
// has been unregistered on all interfaces; so an announcement sent from interface A
// that was already in-flight while unregistering won't be received after the exit on
// interface B.
mAnnouncer.startSending(id, exitInfo, EXIT_ANNOUNCEMENT_DELAY_MS);
} else {
// No exit announcement necessary: remove the service immediately.
mRecordRepository.removeService(id);
if (mRecordRepository.getServicesCount() == 0) {
destroyNow();
}
}
}
/**
* Update interface addresses used to advertise.
*
* This causes new address records to be announced.
*/
public void updateAddresses(@NonNull List<LinkAddress> newAddresses) {
mRecordRepository.updateAddresses(newAddresses);
// TODO: restart advertising, but figure out what exit messages need to be sent for the
// previous addresses
}
/**
* Destroy the advertiser immediately, not sending any exit announcement.
*
* <p>Useful when the underlying network went away. This will trigger an onDestroyed callback.
*/
public void destroyNow() {
for (int serviceId : mRecordRepository.clearServices()) {
mProber.stop(serviceId);
mAnnouncer.stop(serviceId);
}
// TODO: stop receiving packets
mCbHandler.post(() -> mCb.onDestroyed(mSocket));
}
/**
* Reset a service to the probing state due to a conflict found on the network.
*/
public void restartProbingForConflict(int serviceId) {
// TODO: implement
}
/**
* Rename a service following a conflict found on the network, and restart probing.
*/
public void renameServiceForConflict(int serviceId, NsdServiceInfo newInfo) {
// TODO: implement
}
/**
* Indicates whether probing is in progress for the given service on this interface.
*
* Also returns false if the specified service is not registered.
*/
public boolean isProbing(int serviceId) {
return mRecordRepository.isProbing(serviceId);
}
}