diff options
Diffstat (limited to 'PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkDiscovery.java')
-rw-r--r-- | PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkDiscovery.java | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkDiscovery.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkDiscovery.java new file mode 100644 index 0000000..b7b1c6b --- /dev/null +++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkDiscovery.java @@ -0,0 +1,335 @@ +/* + * (c) Copyright 2016 Mopria Alliance, Inc. + * (c) Copyright 2016 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.printservicestubs.servicediscovery; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import com.android.internal.util.Preconditions; +import com.android.printservicestubs.R; +import com.android.printservicestubs.servicediscovery.mdns.MDnsDiscovery; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.MulticastSocket; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; + +/** + * Discovers devices on the network and notifies the listeners about those devices. + */ +public class NetworkDiscovery { + /** + * Currently active clients of the discovery + */ + private static final @NonNull ArrayList<DiscoveryListener> sListeners = new ArrayList<>(); + + /** + * If the network discovery is running, this stores the instance. There can always be at most + * once instance running. + */ + private static @Nullable NetworkDiscovery sInstance = null; + + /** + * Method used for discovering printers + */ + private final @NonNull MDnsDiscovery mDiscoveryMethod; + + /** + * List of discovered printers sorted by device identifier + */ + private final @NonNull LinkedHashMap<String, NetworkDevice> mDiscoveredPrinters; + + /** + * Socket used to discover devices (both query and listen). + */ + private @NonNull MulticastSocket mSocket; + + /** + * Thread sending the broadcasts that should make the device announce them self + */ + private @NonNull QueryThread mQueryThread; + + /** + * Thread that receives the announcements of new devices and processes them + */ + private @NonNull ListenerThread mListenerThread; + + /** + * Register a new listener for network devices. If this is the first listener, this starts a new + * discovery session. + * + * @param listener Listener to register. + * @param context Context the listener is running in. + * + * @throws IOException If the discovery session could not be started + */ + public static void onListenerAdded(@NonNull DiscoveryListener listener, + @NonNull Context context) throws IOException { + listener = Preconditions.checkNotNull(listener, "listener"); + context = Preconditions.checkNotNull(context, "context"); + + synchronized (sListeners) { + sListeners.add(listener); + + if (sInstance == null) { + sInstance = new NetworkDiscovery(context); + } else { + sInstance.onListenerAdded(listener); + } + } + } + + /** + * Remove a previously registered listener for network devices. If this is the last listener, + * the discovery session is terminated. + * + * @param listener The listener to remove + * + * @throws InterruptedException If the thread was interrupted while waiting for the session to + * finish. + */ + public static void removeDiscoveryListener(@NonNull DiscoveryListener listener) + throws InterruptedException { + listener = Preconditions.checkNotNull(listener, "listener"); + + synchronized (sListeners) { + sListeners.remove(listener); + + if (sListeners.isEmpty()) { + sInstance.close(); + sInstance = null; + } + } + } + + /** + * Create and start a new network discovery session. + * + * @param context The context requesting the start of the session. + * + * @throws IOException If the discovery session could not be started + */ + private NetworkDiscovery(@NonNull Context context) throws IOException { + mDiscoveredPrinters = new LinkedHashMap<>(); + + mDiscoveryMethod = new MDnsDiscovery( + context.getResources().getStringArray(R.array.mdns_services)); + + mSocket = NetworkUtils.createMulticastSocket(context, null); + mSocket.setBroadcast(true); + mSocket.setReuseAddress(true); + mSocket.setSoTimeout(0); + + mListenerThread = new ListenerThread(); + mQueryThread = new QueryThread(); + + mListenerThread.start(); + mQueryThread.start(); + } + + /** + * If a new listener was added while the session was already running, announce all already found + * devices to the new listener. + * + * @param listener The listener that was just added. + */ + private void onListenerAdded(@NonNull DiscoveryListener listener) { + synchronized (mDiscoveredPrinters) { + for (NetworkDevice device : mDiscoveredPrinters.values()) { + listener.onDeviceFound(device); + } + } + } + + /** + * Clean up discovery session. + * + * @throws InterruptedException If the current thread was interrupted while the session was + * cleaned up. + */ + private void close() throws InterruptedException { + // Closing the socket causes IOExceptions on operations on this socket. This in turn will + // end the threads. + mSocket.close(); + + mListenerThread.join(); + mQueryThread.join(); + } + + /** + * Notify all currently registered listeners that a new device was removed. + * + * @param networkDevice The device that was removed + */ + private void fireDeviceRemoved(@NonNull NetworkDevice networkDevice) { + synchronized (sListeners) { + final int numListeners = sListeners.size(); + for (int i = 0; i < numListeners; i++) { + sListeners.get(i).onDeviceRemoved(networkDevice); + } + } + } + + /** + * Notify all currently registered listeners that a new device was found. + * + * @param networkDevice The device that was found + */ + private void fireDeviceFound(@NonNull NetworkDevice networkDevice) { + synchronized (sListeners) { + final int numListeners = sListeners.size(); + for (int i = 0; i < numListeners; i++) { + sListeners.get(i).onDeviceFound(networkDevice); + } + } + } + + /** + * Thread receiving and processing the packets that announce network devices + */ + private class ListenerThread extends Thread { + private static final int BUFFER_LENGTH = 4 * 1024; + + @Override + public void run() { + while (true) { + byte[] buffer = new byte[BUFFER_LENGTH]; + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + + try { + mSocket.receive(packet); + + if (packet.getPort() != mDiscoveryMethod.getPort()) { + continue; + } + + ArrayList<ServiceParser> serviceParsers = mDiscoveryMethod + .parseResponse(packet); + + final int numParsers = serviceParsers.size(); + for (int i = 0; i < numParsers; i++) { + ServiceParser parser = serviceParsers.get(i); + NetworkDevice device = new NetworkDevice(parser); + String key = device.getDeviceIdentifier(); + NetworkDevice discoveredNetworkDevice = mDiscoveredPrinters.get(key); + + if (discoveredNetworkDevice != null) { + if(!device.getInetAddress() + .equals(discoveredNetworkDevice.getInetAddress())) { + fireDeviceRemoved(discoveredNetworkDevice); + } else { + discoveredNetworkDevice.addDiscoveryInstance(device); + device = discoveredNetworkDevice; + } + } else { + synchronized (mDiscoveredPrinters) { + mDiscoveredPrinters.put(key, device); + } + } + + fireDeviceFound(device); + } + } catch (IOException e) { + // Socket got closed + break; + } + } + } + } + + /** + * Thread that sends out the packages that make the devices announce them self. The announcement + * are the received by the {@link ListenerThread listener thread}. + */ + private class QueryThread extends Thread { + private static final int MAX_ACTIVE_QUERIES = 10; + private static final int MAX_DELAY_SECONDS = 60; + + private boolean mUseFallback = false; + private boolean mIsActiveDiscovery = true; + private int mQueriesSent = 0; + + /** + * @return the next wait interval, in milliseconds, using an exponential backoff algorithm. + */ + private int getQueryDelayInMillis() { + int delayInSeconds = 1; + + int first = 1; + int second = 1; + int index; + + if (mQueriesSent > MAX_ACTIVE_QUERIES) { + delayInSeconds = MAX_DELAY_SECONDS; + } else { + + for (index = 1; index < mQueriesSent; index++) { + if (index <= 1) { + delayInSeconds = index; + } else { + delayInSeconds = first + second; + first = second; + second = delayInSeconds; + } + } + } + + if (delayInSeconds >= MAX_DELAY_SECONDS) { + delayInSeconds = MAX_DELAY_SECONDS; + if (mIsActiveDiscovery) { + mIsActiveDiscovery = false; + } + } + + return delayInSeconds * 1000; + } + + @Override + public void run() { + mQueriesSent = (mIsActiveDiscovery ? 0 : MAX_ACTIVE_QUERIES); + mUseFallback = false; + + while (true) { + try { + ArrayList<DatagramPacket> datagramList = new ArrayList<>(); + + if (!mDiscoveryMethod.isFallback() || mUseFallback) { + Collections.addAll(datagramList, mDiscoveryMethod.createQueryPackets()); + } + + for (DatagramPacket packet : datagramList) { + mSocket.send(packet); + } + + mQueriesSent++; + + Thread.sleep(getQueryDelayInMillis()); + + if (!mUseFallback) { + mUseFallback = mDiscoveredPrinters.isEmpty(); + } + } catch (Exception e) { + // Socket got closed + break; + } + } + } + } +} |