/* * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License version * 2 only, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License version 2 for more details (a copy is * included at /legal/license.txt). * * You should have received a copy of the GNU General Public License * version 2 along with this work; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa * Clara, CA 95054 or visit www.sun.com if you need additional * information or have any questions. */ package com.sun.jsr082.bluetooth; import java.io.IOException; import javax.bluetooth.BluetoothStateException; import javax.bluetooth.DataElement; import javax.bluetooth.DiscoveryListener; import javax.bluetooth.RemoteDevice; import javax.bluetooth.UUID; import com.sun.jsr082.bluetooth.JavaSDPClient; import java.util.Hashtable; import java.util.Vector; /* * This class saves information about responses to SDP_ServiceSearchRequest and * SDP_ServiceAttributeRequest requests and provides functionality of * DiscoveryAgent.serviceSearch using multiple requests via JavaSDPClient (Service * Discovery Protocol) * */ public class ServiceSearcher extends ServiceSearcherBase implements ServiceDiscoverer { /* Set to false in RR version - then the javac skip the code. */ private static final boolean DEBUG = false; /* this class name for debug. */ private static final String cn = "ServiceSearcher"; /* ID of transaction this listener works in. */ private short transactionID; /* * Listens for service discovery and is informed as a service is discovered. */ private DiscoveryListener discListener; /* SDP client to send requests. */ private JavaSDPClient sdp; /* Service records handles retrieved from a server response. */ private int[] handles; /* Number of service records handles processed. */ private int processedHandle; /* Indicates if this listener is inactive. */ private boolean inactive = false; /* Indicates if service search has been canceled. */ private boolean canceled = false; /* Indicates if listener notification has been called. */ private boolean notified = false; /* Current quantity of service discovering requests. */ private static int requestCounter = 0; /* Keeps the references of current service search requests. */ private static Hashtable searchers = new Hashtable(); /* * Creates ServiceSearcher and save all required info in it. * */ public ServiceSearcher() { } /* * Returns an <code>JavaSDPClient<code> object and opens SDP connection * to the remote device with the specified Bluetooth address. * * @param bluetoothAddress bluetooth address of SDP server */ public SDPClient getSDPClient(String bluetoothAddress) { try { sdp = new JavaSDPClient(bluetoothAddress); } catch (IOException ioe) { } return (SDPClient)sdp; } /* * Start searching services under the given conditions * * @param attrSet * list of attributes whose values are requested. * @param uuidSet * list of UUIDs that indicate services relevant to request. * @param btDev * remote Bluetooth device to listen response from. * @param discListener * discovery listener. */ public int searchService(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev, DiscoveryListener discListener) throws BluetoothStateException, IllegalArgumentException { if (DEBUG) { System.out.println("- serviceSearcher: initializing"); } initialize(attrSet, uuidSet, btDev); if (discListener == null) { throw new NullPointerException("DiscoveryListener is null"); } this.discListener = discListener; return start(); } /* * Starts SDP_ServiceSearchRequest. * * @see JavaSDPClient#serviceSearchRequest * * @return ID of transaction that has been initiated by the request. */ private int start() throws BluetoothStateException { if (DEBUG) { System.out.println("- serviceSearcher: start"); } synchronized (ServiceSearcher.class) { if (requestCounter == ServiceRecordImpl.TRANS_MAX) { throw new BluetoothStateException( "Too much concurrent requests"); } requestCounter++; } transactionID = SDPClientTransaction.newTransactionID(); searchers.put(new Integer(transactionID), this); synchronized (this) { notified = false; } handles = null; processedHandle = 0; try { sdp = new JavaSDPClient(btDev.getBluetoothAddress()); sdp.serviceSearchAttributeRequest(attrSet, uuidSet, transactionID, this); // sdp.serviceSearchRequest(uuidSet, transactionID, this); } catch (IOException ioe) { if (DEBUG) { ioe.printStackTrace(); } synchronized (this) { stop(); new NotifyListenerRunner( DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE); } } if (DEBUG) { System.out.println("- serviceSearch: started with transaction: " + transactionID); } return transactionID; } /* * Receives SDP_ErrorResponse and completes the search request activity by * error reason. * * @param errorCode * error code form SDP_ErrorResponse. * @param info * error details firm SDP_ErrorResponse. * @param transactionID * ID of transaction response got within. */ public void errorResponse(int errorCode, String info, int transactionID) { if (DEBUG) { System.out.println(cn + ".errorResponse: called"); } stop(); if ((errorCode == SDP_INVALID_VERSION) || (errorCode == SDP_INVALID_SYNTAX) || (errorCode == SDP_INVALID_PDU_SIZE) || (errorCode == SDP_INVALID_CONTINUATION_STATE) || (errorCode == SDP_INSUFFICIENT_RESOURCES)) { notifyListener(DiscoveryListener.SERVICE_SEARCH_ERROR); System.err.println(info); } else if (errorCode == SDP_INVALID_SR_HANDLE) { notifyListener(DiscoveryListener.SERVICE_SEARCH_NO_RECORDS); System.err.println(info); } else if (errorCode == IO_ERROR) { notifyListener(DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE); } else if (errorCode == TERMINATED) { new NotifyListenerRunner( DiscoveryListener.SERVICE_SEARCH_TERMINATED); } } /* * Receives array of handles retrieved form SDP_serviceSearchResponse. * * @param handleList * service record handles retrieved from * SDP_srviceSearchResponse. * @param transactionID * ID of transaction response has been received in. */ public void serviceSearchResponse(int[] handleList, int transactionID) { if (DEBUG) { System.out.println(cn + ".serviceSearchResponse: called"); } // there is no reason to perform response processing if search // is canceled if (isCanceled()) { return; } if (handleList == null || handleList.length == 0) { stop(); notifyListener(DiscoveryListener.SERVICE_SEARCH_NO_RECORDS); return; } synchronized (this) { handles = handleList; processedHandle = 0; } try { // there is no reason to request service attributes if service // search is canceled if (isCanceled()) { return; } sdp.serviceAttributeRequest(handles[processedHandle], attrSet, transactionID, this); } catch (IOException ioe) { if (DEBUG) { ioe.printStackTrace(); } stop(); notifyListener(DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE); } } /* * Receives arrays of service record attributes and their values retrieved * from server response. */ public void serviceAttributeResponse(int[] attrIDs, DataElement[] attributeValues, int transactionID) { if (DEBUG) { System.out.println(cn + ".serviceAttributeResponse: called"); } // there is no reason to process service attributes if service // search is canceled if (isCanceled()) { return; } synchronized (this) { processedHandle++; } if (attributeValues != null) { ServiceRecordImpl[] serviceRecordSet = new ServiceRecordImpl[1]; serviceRecordSet[0] = new ServiceRecordImpl(btDev, attrIDs, attributeValues); try { // The spec for DiscoveryAgent.cancelServiceSearch() says: // "After receiving SERVICE_SEARCH_TERMINATED event, // no further servicesDiscovered() events will occur // as a result of this search." if (isCanceled()) { return; } System.out.println("serviceSearch: notify serviceDiscovered"); discListener.servicesDiscovered(this.transactionID, serviceRecordSet); } catch (Throwable e) { e.printStackTrace(); } } if (processedHandle == handles.length) { stop(); if (DEBUG) { System.out .println("serviceSearch: notify service search completed"); } notifyListener(DiscoveryListener.SERVICE_SEARCH_COMPLETED); return; } try { // there is no reason to continue attributes discovery if search // is canceled if (isCanceled()) { return; } sdp.serviceAttributeRequest(handles[processedHandle], attrSet, transactionID, this); } catch (IOException ioe) { if (DEBUG) { ioe.printStackTrace(); } stop(); notifyListener(DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE); } } /* * Base class method not relevant to this subclass, must never be called. */ public void serviceSearchAttributeResponse(int[] attrIDs, DataElement[] attributeValues, int transactionID) { if (DEBUG) { System.out.println(cn + ".serviceSearchAttributeResponse: called"); } // there is no reason to process service attributes if service // search is canceled if (isCanceled()) { return; } if (attrIDs == null || attrIDs.length <= 0 || attributeValues == null || attrIDs.length != attributeValues.length) { notifyListener(DiscoveryListener.SERVICE_SEARCH_NO_RECORDS); return; } int firstPos = 0; Vector responceRecords = new Vector(); for (int i = 1; i < attrIDs.length; i++) { if (attrIDs[i] == 0) { responceRecords.addElement(getOneRecord(attrIDs, attributeValues, firstPos, i)); firstPos = i; } } responceRecords.addElement(getOneRecord(attrIDs, attributeValues, firstPos, attrIDs.length)); ServiceRecordImpl[] records = new ServiceRecordImpl[responceRecords.size()]; for (int i=0; i<responceRecords.size(); i++) { records[i]=(ServiceRecordImpl)responceRecords.elementAt(i); } try { // The spec for DiscoveryAgent.cancelServiceSearch() says: // "After receiving SERVICE_SEARCH_TERMINATED event, // no further servicesDiscovered() events will occur // as a result of this search." if (isCanceled()) { return; } if (DEBUG) { System.out.println("serviceSearch: notify serviceDiscovered"); } discListener.servicesDiscovered(this.transactionID, records); } catch (Throwable e) { e.printStackTrace(); } stop(); if (DEBUG) { System.out .println("serviceSearch: notify service_search_completed"); } notifyListener(DiscoveryListener.SERVICE_SEARCH_COMPLETED); return; } /* * Returns one ServiceRecord from the incoming attributes list */ private ServiceRecordImpl getOneRecord(int[] attrIDs, DataElement[] attributeValues, int firstPos, int curPos) { int aLength = curPos - firstPos; int[] aIDs = new int[aLength]; DataElement[] aValues = new DataElement[aLength]; System.arraycopy(attrIDs, firstPos, aIDs, 0, aLength); System.arraycopy(attributeValues, firstPos, aValues, 0, aLength); ServiceRecordImpl record = new ServiceRecordImpl(btDev, aIDs, aValues); return record; } /* * Finishes the service searcher activity. */ private void stop() { JavaSDPClient sdp; searchers.remove(new Integer(transactionID)); synchronized (this) { if (this.sdp == null) { return; } inactive = true; sdp = this.sdp; this.sdp = null; } synchronized (ServiceSearcher.class) { requestCounter--; } try { sdp.close(); } catch (IOException ioe) { if (DEBUG) { ioe.printStackTrace(); } // ignore } } /* * Cancels current transaction. * * @return false if there is no current transaction, cancels it and returns * true otherwise. */ boolean cancel() { synchronized (this) { if (inactive) { return false; } inactive = true; if (sdp == null) { return false; } if (canceled) { return false; } canceled = true; } // cancel running effective transaction if any. // if sdp.cancelServiceSearch returns false (there is no running // transactions) then call the notification directly. if (!sdp.cancelServiceSearch(transactionID)) { new NotifyListenerRunner( DiscoveryListener.SERVICE_SEARCH_TERMINATED); } return true; } /* * Determines whether the service search has been canceled by the * application and did not complete. * * @return <code>true</code> indicates the service search has been * canceled; <code>false</code> the application has not called * cancel operation */ private boolean isCanceled() { return canceled; } /* * Cancels transaction with given ID. * * @param transactionID * ID of transaction to be cancelled. * * @return false if there is no open transaction with ID given, true * otherwise. */ public boolean cancel(int transactionID) { ServiceSearcher carrier = (ServiceSearcher) searchers.get(new Integer( transactionID)); if (carrier == null) { return false; } else { return carrier.cancel(); } } /* * Notifies the listener that service search has been completed with * specified response code. * * @param respCode * response code. */ private void notifyListener(int respCode) { // guard against multiple notification calls synchronized (this) { if (!notified) { notified = true; } else { return; } } if (DEBUG) { String codeStr = "Undefined"; switch (respCode) { case DiscoveryListener.SERVICE_SEARCH_COMPLETED: codeStr = "SERVICE_SEARCH_COMPLETED"; break; case DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE: codeStr = "SERVICE_SEARCH_DEVICE_NOT_REACHABLE"; break; case DiscoveryListener.SERVICE_SEARCH_ERROR: codeStr = "SERVICE_SEARCH_ERROR"; break; case DiscoveryListener.SERVICE_SEARCH_NO_RECORDS: codeStr = "SERVICE_SEARCH_NO_RECORDS"; break; case DiscoveryListener.SERVICE_SEARCH_TERMINATED: codeStr = "SERVICE_SEARCH_TERMINATED"; break; default: } if (DEBUG) { System.out.println("serviceSearchCompleted:"); System.out.println("\ttransID=" + transactionID); System.out.println("\trespCode=" + codeStr); System.out.println("\tinactive=" + inactive); } } try { discListener.serviceSearchCompleted(transactionID, respCode); } catch (Throwable e) { e.printStackTrace(); } } /* * Runnable for launching <code>notifyListener()</code> in a separate * thread. * * @see #notifyListener(int) */ class NotifyListenerRunner implements Runnable { /* Response code to pass to a listener. */ int respCode; /* * Constructs Runnable instance to pass single response code, starts a * thread for that. * * @param respCode * response code value */ NotifyListenerRunner(int respCode) { this.respCode = respCode; new Thread(this).start(); } /* * The <code>run()</code> method. * * @see java.lang.Runnable */ public void run() { notifyListener(respCode); } } }