diff options
33 files changed, 2005 insertions, 448 deletions
diff --git a/api/current.txt b/api/current.txt index e8d49c3..bea7d1b 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6971,10 +6971,16 @@ package android.bluetooth { public final class BluetoothSocket implements java.io.Closeable { method public void close() throws java.io.IOException; method public void connect() throws java.io.IOException; + method public int getConnectionType(); method public java.io.InputStream getInputStream() throws java.io.IOException; + method public int getMaxReceivePacketSize(); + method public int getMaxTransmitPacketSize(); method public java.io.OutputStream getOutputStream() throws java.io.IOException; method public android.bluetooth.BluetoothDevice getRemoteDevice(); method public boolean isConnected(); + field public static final int TYPE_L2CAP = 3; // 0x3 + field public static final int TYPE_RFCOMM = 1; // 0x1 + field public static final int TYPE_SCO = 2; // 0x2 } } @@ -18108,6 +18114,7 @@ package android.net { public class ConnectivityManager { method public void addDefaultNetworkActiveListener(android.net.ConnectivityManager.OnNetworkActiveListener); method public boolean bindProcessToNetwork(android.net.Network); + method public android.net.Network getActiveNetwork(); method public android.net.NetworkInfo getActiveNetworkInfo(); method public android.net.NetworkInfo[] getAllNetworkInfo(); method public android.net.Network[] getAllNetworks(); @@ -18126,7 +18133,8 @@ package android.net { method public void registerNetworkCallback(android.net.NetworkRequest, android.net.ConnectivityManager.NetworkCallback); method public void releaseNetworkRequest(android.app.PendingIntent); method public void removeDefaultNetworkActiveListener(android.net.ConnectivityManager.OnNetworkActiveListener); - method public void reportBadNetwork(android.net.Network); + method public deprecated void reportBadNetwork(android.net.Network); + method public void reportNetworkConnectivity(android.net.Network, boolean); method public void requestNetwork(android.net.NetworkRequest, android.net.ConnectivityManager.NetworkCallback); method public void requestNetwork(android.net.NetworkRequest, android.app.PendingIntent); method public deprecated boolean requestRouteToHost(int, int); diff --git a/api/system-current.txt b/api/system-current.txt index 0bc83c3..c7d6a20 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -7162,10 +7162,16 @@ package android.bluetooth { public final class BluetoothSocket implements java.io.Closeable { method public void close() throws java.io.IOException; method public void connect() throws java.io.IOException; + method public int getConnectionType(); method public java.io.InputStream getInputStream() throws java.io.IOException; + method public int getMaxReceivePacketSize(); + method public int getMaxTransmitPacketSize(); method public java.io.OutputStream getOutputStream() throws java.io.IOException; method public android.bluetooth.BluetoothDevice getRemoteDevice(); method public boolean isConnected(); + field public static final int TYPE_L2CAP = 3; // 0x3 + field public static final int TYPE_RFCOMM = 1; // 0x1 + field public static final int TYPE_SCO = 2; // 0x2 } } @@ -19566,6 +19572,7 @@ package android.net { public class ConnectivityManager { method public void addDefaultNetworkActiveListener(android.net.ConnectivityManager.OnNetworkActiveListener); method public boolean bindProcessToNetwork(android.net.Network); + method public android.net.Network getActiveNetwork(); method public android.net.NetworkInfo getActiveNetworkInfo(); method public android.net.NetworkInfo[] getAllNetworkInfo(); method public android.net.Network[] getAllNetworks(); @@ -19584,7 +19591,8 @@ package android.net { method public void registerNetworkCallback(android.net.NetworkRequest, android.net.ConnectivityManager.NetworkCallback); method public void releaseNetworkRequest(android.app.PendingIntent); method public void removeDefaultNetworkActiveListener(android.net.ConnectivityManager.OnNetworkActiveListener); - method public void reportBadNetwork(android.net.Network); + method public deprecated void reportBadNetwork(android.net.Network); + method public void reportNetworkConnectivity(android.net.Network, boolean); method public void requestNetwork(android.net.NetworkRequest, android.net.ConnectivityManager.NetworkCallback); method public void requestNetwork(android.net.NetworkRequest, android.app.PendingIntent); method public deprecated boolean requestRouteToHost(int, int); diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index 2b3cf34..87d5bb0 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -1,5 +1,6 @@ /* - * Copyright (C) 2009-2014 The Android Open Source Project + * Copyright (C) 2009-2015 The Android Open Source Project + * Copyright (C) 2015 Samsung LSI * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -374,6 +375,18 @@ public final class BluetoothAdapter { /** @hide */ public static final String BLUETOOTH_MANAGER_SERVICE = "bluetooth_manager"; + + /** When creating a ServerSocket using listenUsingRfcommOn() or + * listenUsingL2capOn() use SOCKET_CHANNEL_AUTO_STATIC to create + * a ServerSocket that auto assigns a channel number to the first + * bluetooth socket. + * The channel number assigned to this first Bluetooth Socket will + * be stored in the ServerSocket, and reused for subsequent Bluetooth + * sockets. + * @hide */ + public static final int SOCKET_CHANNEL_AUTO_STATIC_NO_SDP = -2; + + private static final int ADDRESS_LENGTH = 17; private static final int CONTROLLER_ENERGY_UPDATE_TIMEOUT_MILLIS = 30; @@ -1141,6 +1154,9 @@ public final class BluetoothAdapter { BluetoothServerSocket socket = new BluetoothServerSocket( BluetoothSocket.TYPE_RFCOMM, true, true, channel); int errno = socket.mSocket.bindListen(); + if(channel == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { + socket.setChannel(socket.mSocket.getPort()); + } if (errno != 0) { //TODO(BT): Throw the same exception error code // that the previous code was using. @@ -1275,6 +1291,9 @@ public final class BluetoothAdapter { BluetoothServerSocket socket = new BluetoothServerSocket( BluetoothSocket.TYPE_RFCOMM, false, false, port); int errno = socket.mSocket.bindListen(); + if(port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { + socket.setChannel(socket.mSocket.getPort()); + } if (errno != 0) { //TODO(BT): Throw the same exception error code // that the previous code was using. @@ -1297,6 +1316,9 @@ public final class BluetoothAdapter { BluetoothServerSocket socket = new BluetoothServerSocket( BluetoothSocket.TYPE_RFCOMM, false, true, port); int errno = socket.mSocket.bindListen(); + if(port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { + socket.setChannel(socket.mSocket.getPort()); + } if (errno < 0) { //TODO(BT): Throw the same exception error code // that the previous code was using. @@ -1327,6 +1349,30 @@ public final class BluetoothAdapter { } /** + * Construct an encrypted, authenticated, L2CAP server socket. + * Call #accept to retrieve connections to this socket. + * @return An L2CAP BluetoothServerSocket + * @throws IOException On error, for example Bluetooth not available, or + * insufficient permissions. + * @hide + */ + public BluetoothServerSocket listenUsingL2capOn(int port) throws IOException { + BluetoothServerSocket socket = new BluetoothServerSocket( + BluetoothSocket.TYPE_L2CAP, true, true, port); + int errno = socket.mSocket.bindListen(); + if(port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { + socket.setChannel(socket.mSocket.getPort()); + } + if (errno != 0) { + //TODO(BT): Throw the same exception error code + // that the previous code was using. + //socket.mSocket.throwErrnoNative(errno); + throw new IOException("Error: " + errno); + } + return socket; + } + + /** * Read the local Out of Band Pairing Data * <p>Requires {@link android.Manifest.permission#BLUETOOTH} * diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java index bb0d0a3..1fdf9f4 100644 --- a/core/java/android/bluetooth/BluetoothDevice.java +++ b/core/java/android/bluetooth/BluetoothDevice.java @@ -302,6 +302,12 @@ public final class BluetoothDevice implements Parcelable { */ public static final int DEVICE_TYPE_DUAL = 3; + + /** @hide */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_SDP_RECORD = + "android.bluetooth.device.action.SDP_RECORD"; + /** * Broadcast Action: This intent is used to broadcast the {@link UUID} * wrapped as a {@link android.os.ParcelUuid} of the remote device after it @@ -526,6 +532,13 @@ public final class BluetoothDevice implements Parcelable { */ public static final String EXTRA_UUID = "android.bluetooth.device.extra.UUID"; + /** @hide */ + public static final String EXTRA_SDP_RECORD = + "android.bluetooth.device.extra.SDP_RECORD"; + + /** @hide */ + public static final String EXTRA_SDP_SEARCH_STATUS = + "android.bluetooth.device.extra.SDP_SEARCH_STATUS"; /** * For {@link #getPhonebookAccessPermission}, {@link #setPhonebookAccessPermission}, * {@link #getMessageAccessPermission} and {@link #setMessageAccessPermission}. @@ -1054,14 +1067,34 @@ public final class BluetoothDevice implements Parcelable { return false; } + /** + * Perform a service discovery on the remote device to get the SDP records associated + * with the specified UUID. + * + * <p>This API is asynchronous and {@link #ACTION_SDP_RECORD} intent is sent, + * with the SDP records found on the remote end. If there is an error + * in getting the SDP records or if the process takes a long time, + * {@link #ACTION_SDP_RECORD} intent is sent with an status value in + * {@link #EXTRA_SDP_SEARCH_STATUS} different from 0. + * Detailed status error codes can be found by members of the Bluetooth package in + * the AbstractionLayer class. + * <p>Requires {@link android.Manifest.permission#BLUETOOTH}. + * The SDP record data will be stored in the intent as {@link #EXTRA_SDP_RECORD}. + * The object type will match one of the SdpXxxRecord types, depending on the UUID searched + * for. + * + * @return False if the sanity check fails, True if the process + * of initiating an ACL connection to the remote device + * was started. + */ /** @hide */ - public boolean fetchMasInstances() { + public boolean sdpSearch(ParcelUuid uuid) { if (sService == null) { - Log.e(TAG, "BT not enabled. Cannot query remote device for MAS instances"); + Log.e(TAG, "BT not enabled. Cannot query remote device sdp records"); return false; } try { - return sService.fetchRemoteMasInstances(this); + return sService.sdpSearch(this,uuid); } catch (RemoteException e) {Log.e(TAG, "", e);} return false; } @@ -1261,6 +1294,36 @@ public final class BluetoothDevice implements Parcelable { } /** + * Create an L2cap {@link BluetoothSocket} ready to start a secure + * outgoing connection to this remote device on given channel. + * <p>The remote device will be authenticated and communication on this + * socket will be encrypted. + * <p> Use this socket only if an authenticated socket link is possible. + * Authentication refers to the authentication of the link key to + * prevent man-in-the-middle type of attacks. + * For example, for Bluetooth 2.1 devices, if any of the devices does not + * have an input and output capability or just has the ability to + * display a numeric key, a secure socket connection is not possible. + * In such a case, use {#link createInsecureRfcommSocket}. + * For more details, refer to the Security Model section 5.2 (vol 3) of + * Bluetooth Core Specification version 2.1 + EDR. + * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing + * connection. + * <p>Valid L2CAP PSM channels are in range 1 to 2^16. + * <p>Requires {@link android.Manifest.permission#BLUETOOTH} + * + * @param channel L2cap PSM/channel to connect to + * @return a RFCOMM BluetoothServerSocket ready for an outgoing connection + * @throws IOException on error, for example Bluetooth not available, or + * insufficient permissions + * @hide + */ + public BluetoothSocket createL2capSocket(int channel) throws IOException { + return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP, -1, true, true, this, channel, + null); + } + + /** * Create an RFCOMM {@link BluetoothSocket} ready to start a secure * outgoing connection to this remote device using SDP lookup of uuid. * <p>This is designed to be used with {@link diff --git a/core/java/android/bluetooth/BluetoothServerSocket.java b/core/java/android/bluetooth/BluetoothServerSocket.java index bc56e55..21024a6 100644 --- a/core/java/android/bluetooth/BluetoothServerSocket.java +++ b/core/java/android/bluetooth/BluetoothServerSocket.java @@ -18,6 +18,7 @@ package android.bluetooth; import android.os.Handler; import android.os.ParcelUuid; +import android.util.Log; import java.io.Closeable; import java.io.IOException; @@ -66,10 +67,11 @@ import java.io.IOException; */ public final class BluetoothServerSocket implements Closeable { + private static final String TAG = "BluetoothServerSocket"; /*package*/ final BluetoothSocket mSocket; private Handler mHandler; private int mMessage; - private final int mChannel; + private int mChannel; /** * Construct a socket for incoming connections. @@ -84,6 +86,9 @@ public final class BluetoothServerSocket implements Closeable { throws IOException { mChannel = port; mSocket = new BluetoothSocket(type, -1, auth, encrypt, null, port, null); + if(port == BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { + mSocket.setExcludeSdp(true); + } } /** @@ -98,6 +103,7 @@ public final class BluetoothServerSocket implements Closeable { /*package*/ BluetoothServerSocket(int type, boolean auth, boolean encrypt, ParcelUuid uuid) throws IOException { mSocket = new BluetoothSocket(type, -1, auth, encrypt, null, -1, uuid); + // TODO: This is the same as mChannel = -1 - is this intentional? mChannel = mSocket.getPort(); } @@ -153,6 +159,7 @@ public final class BluetoothServerSocket implements Closeable { /*package*/ void setServiceName(String ServiceName) { mSocket.setServiceName(ServiceName); } + /** * Returns the channel on which this socket is bound. * @hide @@ -160,4 +167,47 @@ public final class BluetoothServerSocket implements Closeable { public int getChannel() { return mChannel; } + + /** + * Sets the channel on which future sockets are bound. + * Currently used only when a channel is auto generated. + */ + /*package*/ void setChannel(int newChannel) { + /* TODO: From a design/architecture perspective this is wrong. + * The bind operation should be conducted through this class + * and the resulting port should be kept in mChannel, and + * not set from BluetoothAdapter. */ + if(mSocket != null) { + if(mSocket.getPort() != newChannel) { + Log.w(TAG,"The port set is different that the underlying port. mSocket.getPort(): " + + mSocket.getPort() + " requested newChannel: " + newChannel); + } + } + mChannel = newChannel; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("ServerSocket: Type: "); + switch(mSocket.getConnectionType()) { + case BluetoothSocket.TYPE_RFCOMM: + { + sb.append("TYPE_RFCOMM"); + break; + } + case BluetoothSocket.TYPE_L2CAP: + { + sb.append("TYPE_L2CAP"); + break; + } + case BluetoothSocket.TYPE_SCO: + { + sb.append("TYPE_SCO"); + break; + } + } + sb.append(" Channel: ").append(mChannel); + return sb.toString(); + } } diff --git a/core/java/android/bluetooth/BluetoothSocket.java b/core/java/android/bluetooth/BluetoothSocket.java index 36997e5..5702d11 100644 --- a/core/java/android/bluetooth/BluetoothSocket.java +++ b/core/java/android/bluetooth/BluetoothSocket.java @@ -21,6 +21,7 @@ import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; +import java.io.BufferedInputStream; import java.io.Closeable; import java.io.FileDescriptor; import java.io.IOException; @@ -29,6 +30,8 @@ import java.io.OutputStream; import java.util.Locale; import java.util.UUID; import android.net.LocalSocket; + +import java.nio.Buffer; import java.nio.ByteOrder; import java.nio.ByteBuffer; /** @@ -86,17 +89,19 @@ public final class BluetoothSocket implements Closeable { /** @hide */ public static final int MAX_RFCOMM_CHANNEL = 30; + /*package*/ static final int MAX_L2CAP_PACKAGE_SIZE = 0xFFFF; /** Keep TYPE_ fields in sync with BluetoothSocket.cpp */ - /*package*/ static final int TYPE_RFCOMM = 1; - /*package*/ static final int TYPE_SCO = 2; - /*package*/ static final int TYPE_L2CAP = 3; + public static final int TYPE_RFCOMM = 1; + public static final int TYPE_SCO = 2; + public static final int TYPE_L2CAP = 3; /*package*/ static final int EBADFD = 77; /*package*/ static final int EADDRINUSE = 98; /*package*/ static final int SEC_FLAG_ENCRYPT = 1; /*package*/ static final int SEC_FLAG_AUTH = 1 << 1; + /*package*/ static final int BTSOCK_FLAG_NO_SDP = 1 << 2; private final int mType; /* one of TYPE_RFCOMM etc */ private BluetoothDevice mDevice; /* remote device */ @@ -106,6 +111,7 @@ public final class BluetoothSocket implements Closeable { private final BluetoothInputStream mInputStream; private final BluetoothOutputStream mOutputStream; private final ParcelUuid mUuid; + private boolean mExcludeSdp = false; private ParcelFileDescriptor mPfd; private LocalSocket mSocket; private InputStream mSocketIS; @@ -115,7 +121,11 @@ public final class BluetoothSocket implements Closeable { private String mServiceName; private static int PROXY_CONNECTION_TIMEOUT = 5000; - private static int SOCK_SIGNAL_SIZE = 16; + private static int SOCK_SIGNAL_SIZE = 20; + + private ByteBuffer mL2capBuffer = null; + private int mMaxTxPacketSize = 0; // The l2cap maximum packet size supported by the peer. + private int mMaxRxPacketSize = 0; // The l2cap maximum packet size that can be received. private enum SocketState { INIT, @@ -144,12 +154,14 @@ public final class BluetoothSocket implements Closeable { */ /*package*/ BluetoothSocket(int type, int fd, boolean auth, boolean encrypt, BluetoothDevice device, int port, ParcelUuid uuid) throws IOException { - if (type == BluetoothSocket.TYPE_RFCOMM && uuid == null && fd == -1) { + if (VDBG) Log.d(TAG, "Creating new BluetoothSocket of type: " + type); + if (type == BluetoothSocket.TYPE_RFCOMM && uuid == null && fd == -1 + && port != BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { if (port < 1 || port > MAX_RFCOMM_CHANNEL) { throw new IOException("Invalid RFCOMM channel: " + port); } } - if(uuid != null) + if (uuid != null) mUuid = uuid; else mUuid = new ParcelUuid(new UUID(0, 0)); mType = type; @@ -172,6 +184,7 @@ public final class BluetoothSocket implements Closeable { mOutputStream = new BluetoothOutputStream(this); } private BluetoothSocket(BluetoothSocket s) { + if (VDBG) Log.d(TAG, "Creating new Private BluetoothSocket of type: " + s.mType); mUuid = s.mUuid; mType = s.mType; mAuth = s.mAuth; @@ -179,7 +192,11 @@ public final class BluetoothSocket implements Closeable { mPort = s.mPort; mInputStream = new BluetoothInputStream(this); mOutputStream = new BluetoothOutputStream(this); + mMaxRxPacketSize = s.mMaxRxPacketSize; + mMaxTxPacketSize = s.mMaxTxPacketSize; + mServiceName = s.mServiceName; + mExcludeSdp = s.mExcludeSdp; } private BluetoothSocket acceptSocket(String RemoteAddr) throws IOException { BluetoothSocket as = new BluetoothSocket(this); @@ -229,6 +246,8 @@ public final class BluetoothSocket implements Closeable { flags |= SEC_FLAG_AUTH; if(mEncrypt) flags |= SEC_FLAG_ENCRYPT; + if(mExcludeSdp) + flags |= BTSOCK_FLAG_NO_SDP; return flags; } @@ -298,7 +317,8 @@ public final class BluetoothSocket implements Closeable { try { if (mSocketState == SocketState.CLOSED) throw new IOException("socket closed"); - IBluetooth bluetoothProxy = BluetoothAdapter.getDefaultAdapter().getBluetoothService(null); + IBluetooth bluetoothProxy = + BluetoothAdapter.getDefaultAdapter().getBluetoothService(null); if (bluetoothProxy == null) throw new IOException("Bluetooth is off"); mPfd = bluetoothProxy.connectSocket(mDevice, mType, mUuid, mPort, getSecurityFlags()); @@ -370,7 +390,7 @@ public final class BluetoothSocket implements Closeable { mSocketState = SocketState.LISTENING; } if (DBG) Log.d(TAG, "channel: " + channel); - if (mPort == -1) { + if (mPort <= -1) { mPort = channel; } // else ASSERT(mPort == channel) ret = 0; @@ -391,7 +411,8 @@ public final class BluetoothSocket implements Closeable { /*package*/ BluetoothSocket accept(int timeout) throws IOException { BluetoothSocket acceptedSocket; - if (mSocketState != SocketState.LISTENING) throw new IOException("bt socket is not in listen state"); + if (mSocketState != SocketState.LISTENING) + throw new IOException("bt socket is not in listen state"); if(timeout > 0) { Log.d(TAG, "accept() set timeout (ms):" + timeout); mSocket.setSoTimeout(timeout); @@ -427,27 +448,80 @@ public final class BluetoothSocket implements Closeable { } /*package*/ int read(byte[] b, int offset, int length) throws IOException { - if (mSocketIS == null) throw new IOException("read is called on null InputStream"); + int ret = 0; if (VDBG) Log.d(TAG, "read in: " + mSocketIS + " len: " + length); - int ret = mSocketIS.read(b, offset, length); - if(ret < 0) + if(mType == TYPE_L2CAP) + { + int bytesToRead = length; + if (VDBG) Log.v(TAG, "l2cap: read(): offset: " + offset + " length:" + length + + "mL2capBuffer= " + mL2capBuffer); + if (mL2capBuffer == null) { + createL2capRxBuffer(); + } + if (mL2capBuffer.remaining() == 0) { + if (VDBG) Log.v(TAG, "l2cap buffer empty, refilling..."); + if (fillL2capRxBuffer() == -1) { + return -1; + } + } + if (bytesToRead > mL2capBuffer.remaining()) { + bytesToRead = mL2capBuffer.remaining(); + } + if(VDBG) Log.v(TAG, "get(): offset: " + offset + + " bytesToRead: " + bytesToRead); + mL2capBuffer.get(b, offset, bytesToRead); + ret = bytesToRead; + }else { + if (VDBG) Log.v(TAG, "default: read(): offset: " + offset + " length:" + length); + ret = mSocketIS.read(b, offset, length); + } + if (ret < 0) throw new IOException("bt socket closed, read return: " + ret); if (VDBG) Log.d(TAG, "read out: " + mSocketIS + " ret: " + ret); return ret; } /*package*/ int write(byte[] b, int offset, int length) throws IOException { - if (mSocketOS == null) throw new IOException("write is called on null OutputStream"); - if (VDBG) Log.d(TAG, "write: " + mSocketOS + " length: " + length); - mSocketOS.write(b, offset, length); - // There is no good way to confirm since the entire process is asynchronous anyway - if (VDBG) Log.d(TAG, "write out: " + mSocketOS + " length: " + length); - return length; + + //TODO: Since bindings can exist between the SDU size and the + // protocol, we might need to throw an exception instead of just + // splitting the write into multiple smaller writes. + // Rfcomm uses dynamic allocation, and should not have any bindings + // to the actual message length. + if (VDBG) Log.d(TAG, "write: " + mSocketOS + " length: " + length); + if (mType == TYPE_L2CAP) { + if(length <= mMaxTxPacketSize) { + mSocketOS.write(b, offset, length); + } else { + int tmpOffset = offset; + int tmpLength = mMaxTxPacketSize; + int endIndex = offset + length; + boolean done = false; + if(DBG) Log.w(TAG, "WARNING: Write buffer larger than L2CAP packet size!\n" + + "Packet will be divided into SDU packets of size " + + mMaxTxPacketSize); + do{ + mSocketOS.write(b, tmpOffset, tmpLength); + tmpOffset += mMaxTxPacketSize; + if((tmpOffset + mMaxTxPacketSize) > endIndex) { + tmpLength = endIndex - tmpOffset; + done = true; + } + } while(!done); + + } + } else { + mSocketOS.write(b, offset, length); + } + // There is no good way to confirm since the entire process is asynchronous anyway + if (VDBG) Log.d(TAG, "write out: " + mSocketOS + " length: " + length); + return length; } @Override public void close() throws IOException { - if (DBG) Log.d(TAG, "close() in, this: " + this + ", channel: " + mPort + ", state: " + mSocketState); + if (DBG) Log.d(TAG, "close() in, this: " + this + ", channel: " + mPort + ", state: " + + mSocketState); if(mSocketState == SocketState.CLOSED) return; else @@ -457,8 +531,9 @@ public final class BluetoothSocket implements Closeable { if(mSocketState == SocketState.CLOSED) return; mSocketState = SocketState.CLOSED; - if (DBG) Log.d(TAG, "close() this: " + this + ", channel: " + mPort + ", mSocketIS: " + mSocketIS + - ", mSocketOS: " + mSocketOS + "mSocket: " + mSocket); + if (DBG) Log.d(TAG, "close() this: " + this + ", channel: " + mPort + + ", mSocketIS: " + mSocketIS + ", mSocketOS: " + mSocketOS + + "mSocket: " + mSocket); if(mSocket != null) { if (DBG) Log.d(TAG, "Closing mSocket: " + mSocket); mSocket.shutdownInput(); @@ -480,6 +555,47 @@ public final class BluetoothSocket implements Closeable { /*package */ int getPort() { return mPort; } + + /** + * Get the maximum supported Transmit packet size for the underlying transport. + * Use this to optimize the writes done to the output socket, to avoid sending + * half full packets. + * @return the maximum supported Transmit packet size for the underlying transport. + */ + public int getMaxTransmitPacketSize(){ + return mMaxTxPacketSize; + } + + /** + * Get the maximum supported Receive packet size for the underlying transport. + * Use this to optimize the reads done on the input stream, as any call to read + * will return a maximum of this amount of bytes - or for some transports a + * multiple of this value. + * @return the maximum supported Receive packet size for the underlying transport. + */ + public int getMaxReceivePacketSize(){ + return mMaxRxPacketSize; + } + + /** + * Get the type of the underlying connection + * @return one of TYPE_ + */ + public int getConnectionType() { + return mType; + } + + /** + * Change if a SDP entry should be automatically created. + * Must be called before calling .bind, for the call to have any effect. + * @param mExcludeSdp <li>TRUE - do not auto generate SDP record. + * <li>FALSE - default - auto generate SPP SDP record. + * @hide + */ + public void setExcludeSdp(boolean excludeSdp) { + this.mExcludeSdp = excludeSdp; + } + private String convertAddr(final byte[] addr) { return String.format(Locale.US, "%02X:%02X:%02X:%02X:%02X:%02X", addr[0] , addr[1], addr[2], addr[3] , addr[4], addr[5]); @@ -487,8 +603,10 @@ public final class BluetoothSocket implements Closeable { private String waitSocketSignal(InputStream is) throws IOException { byte [] sig = new byte[SOCK_SIGNAL_SIZE]; int ret = readAll(is, sig); - if (VDBG) Log.d(TAG, "waitSocketSignal read 16 bytes signal ret: " + ret); + if (VDBG) Log.d(TAG, "waitSocketSignal read " + SOCK_SIGNAL_SIZE + + " bytes signal ret: " + ret); ByteBuffer bb = ByteBuffer.wrap(sig); + /* the struct in native is decorated with __attribute__((packed)), hence this is possible */ bb.order(ByteOrder.nativeOrder()); int size = bb.getShort(); if(size != SOCK_SIGNAL_SIZE) @@ -497,19 +615,36 @@ public final class BluetoothSocket implements Closeable { bb.get(addr); int channel = bb.getInt(); int status = bb.getInt(); + mMaxTxPacketSize = (bb.getShort() & 0xffff); // Convert to unsigned value + mMaxRxPacketSize = (bb.getShort() & 0xffff); // Convert to unsigned value String RemoteAddr = convertAddr(addr); if (VDBG) Log.d(TAG, "waitSocketSignal: sig size: " + size + ", remote addr: " - + RemoteAddr + ", channel: " + channel + ", status: " + status); + + RemoteAddr + ", channel: " + channel + ", status: " + status + + " MaxRxPktSize: " + mMaxRxPacketSize + " MaxTxPktSize: " + mMaxTxPacketSize); if(status != 0) throw new IOException("Connection failure, status: " + status); return RemoteAddr; } + + private void createL2capRxBuffer(){ + if(mType == TYPE_L2CAP) { + // Allocate the buffer to use for reads. + if(VDBG) Log.v(TAG, " Creating mL2capBuffer: mMaxPacketSize: " + mMaxRxPacketSize); + mL2capBuffer = ByteBuffer.wrap(new byte[mMaxRxPacketSize]); + if(VDBG) Log.v(TAG, "mL2capBuffer.remaining()" + mL2capBuffer.remaining()); + mL2capBuffer.limit(0); // Ensure we do a real read at the first read-request + if(VDBG) Log.v(TAG, "mL2capBuffer.remaining() after limit(0):" + + mL2capBuffer.remaining()); + } + } + private int readAll(InputStream is, byte[] b) throws IOException { int left = b.length; while(left > 0) { int ret = is.read(b, b.length - left, left); if(ret <= 0) - throw new IOException("read failed, socket might closed or timeout, read ret: " + ret); + throw new IOException("read failed, socket might closed or timeout, read ret: " + + ret); left -= ret; if(left != 0) Log.w(TAG, "readAll() looping, read partial size: " + (b.length - left) + @@ -526,4 +661,18 @@ public final class BluetoothSocket implements Closeable { bb.order(ByteOrder.nativeOrder()); return bb.getInt(); } + + private int fillL2capRxBuffer() throws IOException { + mL2capBuffer.rewind(); + int ret = mSocketIS.read(mL2capBuffer.array()); + if(ret == -1) { + // reached end of stream - return -1 + mL2capBuffer.limit(0); + return -1; + } + mL2capBuffer.limit(ret); + return ret; + } + + } diff --git a/core/java/android/bluetooth/IBluetooth.aidl b/core/java/android/bluetooth/IBluetooth.aidl index 299f4c8..af560df 100644 --- a/core/java/android/bluetooth/IBluetooth.aidl +++ b/core/java/android/bluetooth/IBluetooth.aidl @@ -68,7 +68,7 @@ interface IBluetooth int getRemoteClass(in BluetoothDevice device); ParcelUuid[] getRemoteUuids(in BluetoothDevice device); boolean fetchRemoteUuids(in BluetoothDevice device); - boolean fetchRemoteMasInstances(in BluetoothDevice device); + boolean sdpSearch(in BluetoothDevice device, in ParcelUuid uuid); boolean setPin(in BluetoothDevice device, boolean accept, int len, in byte[] pinCode); boolean setPasskey(in BluetoothDevice device, boolean accept, int len, in byte[] diff --git a/core/java/android/bluetooth/SdpMasRecord.java b/core/java/android/bluetooth/SdpMasRecord.java new file mode 100644 index 0000000..fa164c0 --- /dev/null +++ b/core/java/android/bluetooth/SdpMasRecord.java @@ -0,0 +1,147 @@ +/* +* Copyright (C) 2015 Samsung System LSI +* 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 android.bluetooth; + +import android.os.Parcel; +import android.os.Parcelable; + +/** @hide */ +public class SdpMasRecord implements Parcelable { + private final int mMasInstanceId; + private final int mL2capPsm; + private final int mRfcommChannelNumber; + private final int mProfileVersion; + private final int mSupportedFeatures; + private final int mSupportedMessageTypes; + private final String mServiceName; + public static final class MessageType { + public static final int EMAIL = 0x01; + public static final int SMS_GSM = 0x02; + public static final int SMS_CDMA = 0x04; + public static final int MMS = 0x08; + } + + public SdpMasRecord(int mas_instance_id, + int l2cap_psm, + int rfcomm_channel_number, + int profile_version, + int supported_features, + int supported_message_types, + String service_name){ + this.mMasInstanceId = mas_instance_id; + this.mL2capPsm = l2cap_psm; + this.mRfcommChannelNumber = rfcomm_channel_number; + this.mProfileVersion = profile_version; + this.mSupportedFeatures = supported_features; + this.mSupportedMessageTypes = supported_message_types; + this.mServiceName = service_name; + } + + public SdpMasRecord(Parcel in){ + this.mMasInstanceId = in.readInt(); + this.mL2capPsm = in.readInt(); + this.mRfcommChannelNumber = in.readInt(); + this.mProfileVersion = in.readInt(); + this.mSupportedFeatures = in.readInt(); + this.mSupportedMessageTypes = in.readInt(); + this.mServiceName = in.readString(); + } + @Override + public int describeContents() { + // TODO Auto-generated method stub + return 0; + } + + public int getMasInstanceId() { + return mMasInstanceId; + } + + public int getL2capPsm() { + return mL2capPsm; + } + + public int getRfcommCannelNumber() { + return mRfcommChannelNumber; + } + + public int getProfileVersion() { + return mProfileVersion; + } + + public int getSupportedFeatures() { + return mSupportedFeatures; + } + + public int getSupportedMessageTypes() { + return mSupportedMessageTypes; + } + + public boolean msgSupported(int msg) { + return (mSupportedMessageTypes & msg) != 0; + } + + public String getServiceName() { + return mServiceName; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + + dest.writeInt(this.mMasInstanceId); + dest.writeInt(this.mL2capPsm); + dest.writeInt(this.mRfcommChannelNumber); + dest.writeInt(this.mProfileVersion); + dest.writeInt(this.mSupportedFeatures); + dest.writeInt(this.mSupportedMessageTypes); + dest.writeString(this.mServiceName); + + } + @Override + public String toString(){ + String ret = "Bluetooth MAS SDP Record:\n"; + + if(mMasInstanceId != -1){ + ret += "Mas Instance Id: " + mMasInstanceId + "\n"; + } + if(mRfcommChannelNumber != -1){ + ret += "RFCOMM Chan Number: " + mRfcommChannelNumber + "\n"; + } + if(mL2capPsm != -1){ + ret += "L2CAP PSM: " + mL2capPsm + "\n"; + } + if(mServiceName != null){ + ret += "Service Name: " + mServiceName + "\n"; + } + if(mProfileVersion != -1){ + ret += "Profile version: " + mProfileVersion + "\n"; + } + if(mSupportedMessageTypes != -1){ + ret += "Supported msg types: " + mSupportedMessageTypes + "\n"; + } + if(mSupportedFeatures != -1){ + ret += "Supported features: " + mSupportedFeatures + "\n"; + } + return ret; + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public SdpMasRecord createFromParcel(Parcel in) { + return new SdpMasRecord(in); + } + public SdpRecord[] newArray(int size) { + return new SdpRecord[size]; + } + }; +} diff --git a/core/java/android/bluetooth/SdpMnsRecord.java b/core/java/android/bluetooth/SdpMnsRecord.java new file mode 100644 index 0000000..c02bb5a --- /dev/null +++ b/core/java/android/bluetooth/SdpMnsRecord.java @@ -0,0 +1,112 @@ +/* +* Copyright (C) 2015 Samsung System LSI +* 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 android.bluetooth; + +import android.os.Parcel; +import android.os.Parcelable; + +/** @hide */ +public class SdpMnsRecord implements Parcelable { + private final int mL2capPsm; + private final int mRfcommChannelNumber; + private final int mSupportedFeatures; + private final int mProfileVersion; + private final String mServiceName; + + public SdpMnsRecord(int l2cap_psm, + int rfcomm_channel_number, + int profile_version, + int supported_features, + String service_name){ + this.mL2capPsm = l2cap_psm; + this.mRfcommChannelNumber = rfcomm_channel_number; + this.mSupportedFeatures = supported_features; + this.mServiceName = service_name; + this.mProfileVersion = profile_version; + } + + public SdpMnsRecord(Parcel in){ + this.mRfcommChannelNumber = in.readInt(); + this.mL2capPsm = in.readInt(); + this.mServiceName = in.readString(); + this.mSupportedFeatures = in.readInt(); + this.mProfileVersion = in.readInt(); + } + @Override + public int describeContents() { + // TODO Auto-generated method stub + return 0; + } + + + public int getL2capPsm() { + return mL2capPsm; + } + + public int getRfcommChannelNumber() { + return mRfcommChannelNumber; + } + + public int getSupportedFeatures() { + return mSupportedFeatures; + } + + public String getServiceName() { + return mServiceName; + } + + public int getProfileVersion() { + return mProfileVersion; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRfcommChannelNumber); + dest.writeInt(mL2capPsm); + dest.writeString(mServiceName); + dest.writeInt(mSupportedFeatures); + dest.writeInt(mProfileVersion); + } + + public String toString(){ + String ret = "Bluetooth MNS SDP Record:\n"; + + if(mRfcommChannelNumber != -1){ + ret += "RFCOMM Chan Number: " + mRfcommChannelNumber + "\n"; + } + if(mL2capPsm != -1){ + ret += "L2CAP PSM: " + mL2capPsm + "\n"; + } + if(mServiceName != null){ + ret += "Service Name: " + mServiceName + "\n"; + } + if(mSupportedFeatures != -1){ + ret += "Supported features: " + mSupportedFeatures + "\n"; + } + if(mProfileVersion != -1){ + ret += "Profile_version: " + mProfileVersion+"\n"; + } + return ret; + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public SdpMnsRecord createFromParcel(Parcel in) { + return new SdpMnsRecord(in); + } + public SdpMnsRecord[] newArray(int size) { + return new SdpMnsRecord[size]; + } + }; +} diff --git a/core/java/android/bluetooth/SdpOppOpsRecord.java b/core/java/android/bluetooth/SdpOppOpsRecord.java new file mode 100644 index 0000000..e0e4007 --- /dev/null +++ b/core/java/android/bluetooth/SdpOppOpsRecord.java @@ -0,0 +1,118 @@ +/* +* Copyright (C) 2015 Samsung System LSI +* 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 android.bluetooth; + +import java.util.Arrays; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Data representation of a Object Push Profile Server side SDP record. + */ +/** @hide */ +public class SdpOppOpsRecord implements Parcelable { + + private final String mServiceName; + private final int mRfcommChannel; + private final int mL2capPsm; + private final int mProfileVersion; + private final byte[] mFormatsList; + + public SdpOppOpsRecord(String serviceName, int rfcommChannel, + int l2capPsm, int version, byte[] formatsList) { + super(); + this.mServiceName = serviceName; + this.mRfcommChannel = rfcommChannel; + this.mL2capPsm = l2capPsm; + this.mProfileVersion = version; + this.mFormatsList = formatsList; + } + + public String getServiceName() { + return mServiceName; + } + + public int getRfcommChannel() { + return mRfcommChannel; + } + + public int getL2capPsm() { + return mL2capPsm; + } + + public int getProfileVersion() { + return mProfileVersion; + } + + public byte[] getFormatsList() { + return mFormatsList; + } + + @Override + public int describeContents() { + /* No special objects */ + return 0; + } + + public SdpOppOpsRecord(Parcel in){ + this.mRfcommChannel = in.readInt(); + this.mL2capPsm = in.readInt(); + this.mProfileVersion = in.readInt(); + this.mServiceName = in.readString(); + int arrayLength = in.readInt(); + if(arrayLength > 0) { + byte[] bytes = new byte[arrayLength]; + in.readByteArray(bytes); + this.mFormatsList = bytes; + } else { + this.mFormatsList = null; + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRfcommChannel); + dest.writeInt(mL2capPsm); + dest.writeInt(mProfileVersion); + dest.writeString(mServiceName); + if(mFormatsList!= null && mFormatsList.length > 0) { + dest.writeInt(mFormatsList.length); + dest.writeByteArray(mFormatsList); + } else { + dest.writeInt(0); + } + } + + public String toString(){ + StringBuilder sb = new StringBuilder("Bluetooth OPP Server SDP Record:\n"); + sb.append(" RFCOMM Chan Number: ").append(mRfcommChannel); + sb.append("\n L2CAP PSM: ").append(mL2capPsm); + sb.append("\n Profile version: ").append(mProfileVersion); + sb.append("\n Service Name: ").append(mServiceName); + sb.append("\n Formats List: ").append(Arrays.toString(mFormatsList)); + return sb.toString(); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public SdpOppOpsRecord createFromParcel(Parcel in) { + return new SdpOppOpsRecord(in); + } + public SdpOppOpsRecord[] newArray(int size) { + return new SdpOppOpsRecord[size]; + } + }; + +} diff --git a/core/java/android/bluetooth/SdpPseRecord.java b/core/java/android/bluetooth/SdpPseRecord.java new file mode 100644 index 0000000..2c159cc --- /dev/null +++ b/core/java/android/bluetooth/SdpPseRecord.java @@ -0,0 +1,125 @@ +/* +* Copyright (C) 2015 Samsung System LSI +* 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 android.bluetooth; + +import android.os.Parcel; +import android.os.Parcelable; + +/** @hide */ +public class SdpPseRecord implements Parcelable { + private final int mL2capPsm; + private final int mRfcommChannelNumber; + private final int mProfileVersion; + private final int mSupportedFeatures; + private final int mSupportedRepositories; + private final String mServiceName; + + public SdpPseRecord(int l2cap_psm, + int rfcomm_channel_number, + int profile_version, + int supported_features, + int supported_repositories, + String service_name){ + this.mL2capPsm = l2cap_psm; + this.mRfcommChannelNumber = rfcomm_channel_number; + this.mProfileVersion = profile_version; + this.mSupportedFeatures = supported_features; + this.mSupportedRepositories = supported_repositories; + this.mServiceName = service_name; + } + + public SdpPseRecord(Parcel in){ + this.mRfcommChannelNumber = in.readInt(); + this.mL2capPsm = in.readInt(); + this.mProfileVersion = in.readInt(); + this.mSupportedFeatures = in.readInt(); + this.mSupportedRepositories = in.readInt(); + this.mServiceName = in.readString(); + } + @Override + public int describeContents() { + // TODO Auto-generated method stub + return 0; + } + + public int getL2capPsm() { + return mL2capPsm; + } + + public int getRfcommChannelNumber() { + return mRfcommChannelNumber; + } + + public int getSupportedFeatures() { + return mSupportedFeatures; + } + + public String getServiceName() { + return mServiceName; + } + + public int getProfileVersion() { + return mProfileVersion; + } + + public int getSupportedRepositories() { + return mSupportedRepositories; + } + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRfcommChannelNumber); + dest.writeInt(mL2capPsm); + dest.writeInt(mProfileVersion); + dest.writeInt(mSupportedFeatures); + dest.writeInt(mSupportedRepositories); + dest.writeString(mServiceName); + + } + + public String toString(){ + String ret = "Bluetooth MNS SDP Record:\n"; + + if(mRfcommChannelNumber != -1){ + ret += "RFCOMM Chan Number: " + mRfcommChannelNumber + "\n"; + } + if(mL2capPsm != -1){ + ret += "L2CAP PSM: " + mL2capPsm + "\n"; + } + if(mProfileVersion != -1){ + ret += "profile version: " + mProfileVersion + "\n"; + } + if(mServiceName != null){ + ret += "Service Name: " + mServiceName + "\n"; + } + if(mSupportedFeatures != -1){ + ret += "Supported features: " + mSupportedFeatures + "\n"; + } + if(mSupportedRepositories != -1){ + ret += "Supported repositories: " + mSupportedRepositories + "\n"; + } + + return ret; + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public SdpPseRecord createFromParcel(Parcel in) { + return new SdpPseRecord(in); + } + public SdpPseRecord[] newArray(int size) { + return new SdpPseRecord[size]; + } + }; +} diff --git a/core/java/android/bluetooth/SdpRecord.java b/core/java/android/bluetooth/SdpRecord.java new file mode 100644 index 0000000..6f1065e --- /dev/null +++ b/core/java/android/bluetooth/SdpRecord.java @@ -0,0 +1,76 @@ +/* +* Copyright (C) 2015 Samsung System LSI +* 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 android.bluetooth; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Arrays; + +/** @hide */ +public class SdpRecord implements Parcelable{ + + private final byte[] mRawData; + private final int mRawSize; + + @Override + public String toString() { + return "BluetoothSdpRecord [rawData=" + Arrays.toString(mRawData) + + ", rawSize=" + mRawSize + "]"; + } + + public SdpRecord(int size_record, byte[] record){ + this.mRawData = record; + this.mRawSize = size_record; + } + + public SdpRecord(Parcel in){ + this.mRawSize = in.readInt(); + this.mRawData = new byte[mRawSize]; + in.readByteArray(this.mRawData); + + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(this.mRawSize); + dest.writeByteArray(this.mRawData); + + + } + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public SdpRecord createFromParcel(Parcel in) { + return new SdpRecord(in); + } + + public SdpRecord[] newArray(int size) { + return new SdpRecord[size]; + } + }; + + public byte[] getRawData() { + return mRawData; + } + + public int getRawSize() { + return mRawSize; + } +} diff --git a/core/java/android/bluetooth/le/BluetoothLeScanner.java b/core/java/android/bluetooth/le/BluetoothLeScanner.java index b6bcfb8..3078951 100644 --- a/core/java/android/bluetooth/le/BluetoothLeScanner.java +++ b/core/java/android/bluetooth/le/BluetoothLeScanner.java @@ -409,8 +409,8 @@ public final class BluetoothLeScanner { List <ScanFilter> filterList) { final int callbackType = settings.getCallbackType(); // If onlost/onfound is requested, a non-empty filter is expected - if ((callbackType & ScanSettings.CALLBACK_TYPE_FIRST_MATCH - | ScanSettings.CALLBACK_TYPE_MATCH_LOST) != 0) { + if ((callbackType & (ScanSettings.CALLBACK_TYPE_FIRST_MATCH + | ScanSettings.CALLBACK_TYPE_MATCH_LOST)) != 0) { if (filterList == null) { return false; } diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 55e39b1..ce1b01e 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -611,6 +611,27 @@ public class ConnectivityManager { } /** + * Returns a {@link Network} object corresponding to the currently active + * default data network. In the event that the current active default data + * network disconnects, the returned {@code Network} object will no longer + * be usable. This will return {@code null} when there is no default + * network. + * + * @return a {@link Network} object for the current default network or + * {@code null} if no default network is currently active + * + * <p>This method requires the caller to hold the permission + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. + */ + public Network getActiveNetwork() { + try { + return mService.getActiveNetwork(); + } catch (RemoteException e) { + return null; + } + } + + /** * Returns details about the currently active default data network * for a given uid. This is for internal use only to avoid spying * other apps. @@ -831,48 +852,6 @@ public class ConnectivityManager { } /** - * Tells each network type to set its radio power state as directed. - * - * @param turnOn a boolean, {@code true} to turn the radios on, - * {@code false} to turn them off. - * @return a boolean, {@code true} indicating success. All network types - * will be tried, even if some fail. - * - * <p>This method requires the caller to hold the permission - * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}. - * {@hide} - */ -// TODO - check for any callers and remove -// public boolean setRadios(boolean turnOn) { -// try { -// return mService.setRadios(turnOn); -// } catch (RemoteException e) { -// return false; -// } -// } - - /** - * Tells a given networkType to set its radio power state as directed. - * - * @param networkType the int networkType of interest. - * @param turnOn a boolean, {@code true} to turn the radio on, - * {@code} false to turn it off. - * @return a boolean, {@code true} indicating success. - * - * <p>This method requires the caller to hold the permission - * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}. - * {@hide} - */ -// TODO - check for any callers and remove -// public boolean setRadio(int networkType, boolean turnOn) { -// try { -// return mService.setRadio(networkType, turnOn); -// } catch (RemoteException e) { -// return false; -// } -// } - - /** * Tells the underlying networking system that the caller wants to * begin using the named feature. The interpretation of {@code feature} * is completely up to each networking implementation. @@ -1738,10 +1717,33 @@ public class ConnectivityManager { * * @param network The {@link Network} the application was attempting to use * or {@code null} to indicate the current default network. + * @deprecated Use {@link #reportNetworkConnectivity} which allows reporting both + * working and non-working connectivity. */ public void reportBadNetwork(Network network) { try { - mService.reportBadNetwork(network); + // One of these will be ignored because it matches system's current state. + // The other will trigger the necessary reevaluation. + mService.reportNetworkConnectivity(network, true); + mService.reportNetworkConnectivity(network, false); + } catch (RemoteException e) { + } + } + + /** + * Report to the framework whether a network has working connectivity. + * This provides a hint to the system that a particular network is providing + * working connectivity or not. In response the framework may re-evaluate + * the network's connectivity and might take further action thereafter. + * + * @param network The {@link Network} the application was attempting to use + * or {@code null} to indicate the current default network. + * @param hasConnectivity {@code true} if the application was able to successfully access the + * Internet using {@code network} or {@code false} if not. + */ + public void reportNetworkConnectivity(Network network, boolean hasConnectivity) { + try { + mService.reportNetworkConnectivity(network, hasConnectivity); } catch (RemoteException e) { } } @@ -1978,12 +1980,18 @@ public class ConnectivityManager { } catch (RemoteException e) { } } - /** {@hide} */ - public void registerNetworkAgent(Messenger messenger, NetworkInfo ni, LinkProperties lp, + /** + * @hide + * Register a NetworkAgent with ConnectivityService. + * @return NetID corresponding to NetworkAgent. + */ + public int registerNetworkAgent(Messenger messenger, NetworkInfo ni, LinkProperties lp, NetworkCapabilities nc, int score, NetworkMisc misc) { try { - mService.registerNetworkAgent(messenger, ni, lp, nc, score, misc); - } catch (RemoteException e) { } + return mService.registerNetworkAgent(messenger, ni, lp, nc, score, misc); + } catch (RemoteException e) { + return NETID_UNSET; + } } /** diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index 1aa9c45..055f1ab 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -43,6 +43,7 @@ import com.android.internal.net.VpnProfile; /** {@hide} */ interface IConnectivityManager { + Network getActiveNetwork(); NetworkInfo getActiveNetworkInfo(); NetworkInfo getActiveNetworkInfoForUid(int uid); NetworkInfo getNetworkInfo(int networkType); @@ -95,7 +96,7 @@ interface IConnectivityManager void reportInetCondition(int networkType, int percentage); - void reportBadNetwork(in Network network); + void reportNetworkConnectivity(in Network network, boolean hasConnectivity); ProxyInfo getGlobalProxy(); @@ -121,8 +122,6 @@ interface IConnectivityManager void captivePortalCheckCompleted(in NetworkInfo info, boolean isCaptivePortal); - int findConnectionTypeForIface(in String iface); - int checkMobileProvisioning(int suggestedTimeOutMs); String getMobileProvisioningUrl(); @@ -137,7 +136,7 @@ interface IConnectivityManager void unregisterNetworkFactory(in Messenger messenger); - void registerNetworkAgent(in Messenger messenger, in NetworkInfo ni, in LinkProperties lp, + int registerNetworkAgent(in Messenger messenger, in NetworkInfo ni, in LinkProperties lp, in NetworkCapabilities nc, int score, in NetworkMisc misc); NetworkRequest requestNetwork(in NetworkCapabilities networkCapabilities, diff --git a/core/java/android/net/NetworkAgent.java b/core/java/android/net/NetworkAgent.java index 95ceb2a..3f2dd28 100644 --- a/core/java/android/net/NetworkAgent.java +++ b/core/java/android/net/NetworkAgent.java @@ -39,6 +39,10 @@ import java.util.ArrayList; * @hide */ public abstract class NetworkAgent extends Handler { + // Guaranteed to be valid (not NETID_UNSET), otherwise registerNetworkAgent() would have thrown + // an exception. + public final int netId; + private volatile AsyncChannel mAsyncChannel; private final String LOG_TAG; private static final boolean DBG = true; @@ -151,7 +155,7 @@ public abstract class NetworkAgent extends Handler { if (VDBG) log("Registering NetworkAgent"); ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService( Context.CONNECTIVITY_SERVICE); - cm.registerNetworkAgent(new Messenger(this), new NetworkInfo(ni), + netId = cm.registerNetworkAgent(new Messenger(this), new NetworkInfo(ni), new LinkProperties(lp), new NetworkCapabilities(nc), score, misc); } diff --git a/obex/Android.mk b/obex/Android.mk index fbfe9be..e7c1fd3 100644 --- a/obex/Android.mk +++ b/obex/Android.mk @@ -7,3 +7,14 @@ LOCAL_SRC_FILES := $(call all-subdir-java-files) LOCAL_MODULE:= javax.obex include $(BUILD_JAVA_LIBRARY) + + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_MODULE:= javax.obexstatic + +include $(BUILD_STATIC_JAVA_LIBRARY)
\ No newline at end of file diff --git a/obex/javax/obex/ClientOperation.java b/obex/javax/obex/ClientOperation.java index 75278b5..cc20d39 100644 --- a/obex/javax/obex/ClientOperation.java +++ b/obex/javax/obex/ClientOperation.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 2014 The Android Open Source Project + * Copyright (c) 2015 The Android Open Source Project + * Copyright (C) 2015 Samsung LSI * Copyright (c) 2008-2009, Motorola, Inc. * * All rights reserved. @@ -40,6 +41,8 @@ import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.ByteArrayOutputStream; +import android.util.Log; + /** * This class implements the <code>Operation</code> interface. It will read and * write data via puts and gets. @@ -47,6 +50,10 @@ import java.io.ByteArrayOutputStream; */ public final class ClientOperation implements Operation, BaseStream { + private static final String TAG = "ClientOperation"; + + private static final boolean V = ObexHelper.VDBG; + private ClientSession mParent; private boolean mInputOpen; @@ -75,6 +82,19 @@ public final class ClientOperation implements Operation, BaseStream { private boolean mEndOfBodySent; + private boolean mSendBodyHeader = true; + // A latch - when triggered, there is not way back ;-) + private boolean mSrmActive = false; + + // Assume SRM disabled - until support is confirmed + // by the server + private boolean mSrmEnabled = false; + // keep waiting until final-bit is received in request + // to handle the case where the SRM enable header is in + // a different OBEX packet than the SRMP header. + private boolean mSrmWaitingForRemote = true; + + /** * Creates new OperationImpl to read and write data to a server * @param maxSize the maximum packet size @@ -164,7 +184,7 @@ public final class ClientOperation implements Operation, BaseStream { * Since we are not sending any headers or returning any headers then * we just need to write and read the same bytes */ - mParent.sendRequest(ObexHelper.OBEX_OPCODE_ABORT, null, mReplyHeader, null); + mParent.sendRequest(ObexHelper.OBEX_OPCODE_ABORT, null, mReplyHeader, null, false); if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_OK) { throw new IOException("Invalid response code from server"); @@ -215,6 +235,7 @@ public final class ClientOperation implements Operation, BaseStream { try { return (String)mReplyHeader.getHeader(HeaderSet.TYPE); } catch (IOException e) { + if(V) Log.d(TAG, "Exception occured - returning null",e); return null; } } @@ -236,6 +257,7 @@ public final class ClientOperation implements Operation, BaseStream { return temp.longValue(); } } catch (IOException e) { + if(V) Log.d(TAG,"Exception occured - returning -1",e); return -1; } } @@ -408,7 +430,9 @@ public final class ClientOperation implements Operation, BaseStream { } /** - * Sends a request to the client of the specified type + * Sends a request to the client of the specified type. + * This function will enable SRM and set SRM active if the server + * response allows this. * @param opCode the request code to send to the client * @return <code>true</code> if there is more data to send; * <code>false</code> if there is no more data to send @@ -431,13 +455,16 @@ public final class ClientOperation implements Operation, BaseStream { * length, but it is a waste of resources if we can't send much of * the body. */ - if ((ObexHelper.BASE_PACKET_LENGTH + headerArray.length) > mMaxPacketSize) { + final int MINIMUM_BODY_LENGTH = 3; + if ((ObexHelper.BASE_PACKET_LENGTH + headerArray.length + MINIMUM_BODY_LENGTH) + > mMaxPacketSize) { int end = 0; int start = 0; // split & send the headerArray in multiple packets. while (end != headerArray.length) { //split the headerArray + end = ObexHelper.findHeaderEnd(headerArray, start, mMaxPacketSize - ObexHelper.BASE_PACKET_LENGTH); // can not split @@ -459,7 +486,7 @@ public final class ClientOperation implements Operation, BaseStream { byte[] sendHeader = new byte[end - start]; System.arraycopy(headerArray, start, sendHeader, 0, sendHeader.length); - if (!mParent.sendRequest(opCode, sendHeader, mReplyHeader, mPrivateInput)) { + if (!mParent.sendRequest(opCode, sendHeader, mReplyHeader, mPrivateInput, false)) { return false; } @@ -470,12 +497,20 @@ public final class ClientOperation implements Operation, BaseStream { start = end; } + // Enable SRM if it should be enabled + checkForSrm(); + if (bodyLength > 0) { return true; } else { return false; } } else { + /* All headers will fit into a single package */ + if(mSendBodyHeader == false) { + /* As we are not to send any body data, set the FINAL_BIT */ + opCode |= ObexHelper.OBEX_OPCODE_FINAL_BIT_MASK; + } out.write(headerArray); } @@ -499,11 +534,11 @@ public final class ClientOperation implements Operation, BaseStream { * (End of Body) otherwise, we need to send 0x48 (Body) */ if ((mPrivateOutput.isClosed()) && (!returnValue) && (!mEndOfBodySent) - && ((opCode & 0x80) != 0)) { - out.write(0x49); + && ((opCode & ObexHelper.OBEX_OPCODE_FINAL_BIT_MASK) != 0)) { + out.write(HeaderSet.END_OF_BODY); mEndOfBodySent = true; } else { - out.write(0x48); + out.write(HeaderSet.BODY); } bodyLength += 3; @@ -517,12 +552,11 @@ public final class ClientOperation implements Operation, BaseStream { if (mPrivateOutputOpen && bodyLength <= 0 && !mEndOfBodySent) { // only 0x82 or 0x83 can send 0x49 - if ((opCode & 0x80) == 0) { - out.write(0x48); + if ((opCode & ObexHelper.OBEX_OPCODE_FINAL_BIT_MASK) == 0) { + out.write(HeaderSet.BODY); } else { - out.write(0x49); + out.write(HeaderSet.END_OF_BODY); mEndOfBodySent = true; - } bodyLength = 3; @@ -531,15 +565,20 @@ public final class ClientOperation implements Operation, BaseStream { } if (out.size() == 0) { - if (!mParent.sendRequest(opCode, null, mReplyHeader, mPrivateInput)) { + if (!mParent.sendRequest(opCode, null, mReplyHeader, mPrivateInput, mSrmActive)) { return false; } + // Enable SRM if it should be enabled + checkForSrm(); return returnValue; } if ((out.size() > 0) - && (!mParent.sendRequest(opCode, out.toByteArray(), mReplyHeader, mPrivateInput))) { + && (!mParent.sendRequest(opCode, out.toByteArray(), + mReplyHeader, mPrivateInput, mSrmActive))) { return false; } + // Enable SRM if it should be enabled + checkForSrm(); // send all of the output data in 0x48, // send 0x49 with empty body @@ -549,6 +588,35 @@ public final class ClientOperation implements Operation, BaseStream { return returnValue; } + private void checkForSrm() throws IOException { + Byte srmMode = (Byte)mReplyHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if(mParent.isSrmSupported() == true && srmMode != null + && srmMode == ObexHelper.OBEX_SRM_ENABLE) { + mSrmEnabled = true; + } + /** + * Call this only when a complete obex packet have been received. + * (This is not optimal, but the current design is not really suited to + * the way SRM is specified.) + * The BT usage of SRM is not really safe - it assumes that the SRMP will fit + * into every OBEX packet, hence if another header occupies the entire packet, + * the scheme will not work - unlikely though. + */ + if(mSrmEnabled) { + mSrmWaitingForRemote = false; + Byte srmp = (Byte)mReplyHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + if(srmp != null && srmp == ObexHelper.OBEX_SRMP_WAIT) { + mSrmWaitingForRemote = true; + // Clear the wait header, as the absence of the header in the next packet + // indicates don't wait anymore. + mReplyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, null); + } + } + if((mSrmWaitingForRemote == false) && (mSrmEnabled == true)) { + mSrmActive = true; + } + } + /** * This method starts the processing thread results. It will send the * initial request. If the response takes more then one packet, a thread @@ -564,40 +632,35 @@ public final class ClientOperation implements Operation, BaseStream { if (mGetOperation) { if (!mOperationDone) { - if (!mGetFinalFlag) { - mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; - while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { - more = sendRequest(0x03); - } - - if (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { - mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput); - } - if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { - mOperationDone = true; - } - } else { - more = sendRequest(0x83); - - if (more) { - throw new IOException("FINAL_GET forced but data did not fit into single packet!"); - } - + mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; + while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { + more = sendRequest(ObexHelper.OBEX_OPCODE_GET); + } + // For GET we need to loop until all headers have been sent, + // And then we wait for the first continue package with the + // reply. + if (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { + mParent.sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL, + null, mReplyHeader, mPrivateInput, mSrmActive); + } + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { mOperationDone = true; + } else { + checkForSrm(); } } } else { - + // PUT operation if (!mOperationDone) { mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { - more = sendRequest(0x02); - + more = sendRequest(ObexHelper.OBEX_OPCODE_PUT); } } if (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { - mParent.sendRequest(0x82, null, mReplyHeader, mPrivateInput); + mParent.sendRequest(ObexHelper.OBEX_OPCODE_PUT_FINAL, + null, mReplyHeader, mPrivateInput, mSrmActive); } if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { @@ -617,15 +680,21 @@ public final class ClientOperation implements Operation, BaseStream { public synchronized boolean continueOperation(boolean sendEmpty, boolean inStream) throws IOException { + // One path to the first put operation - the other one does not need to + // handle SRM, as all will fit into one packet. + if (mGetOperation) { if ((inStream) && (!mOperationDone)) { // to deal with inputstream in get operation - mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput); + mParent.sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL, + null, mReplyHeader, mPrivateInput, mSrmActive); /* * Determine if that was not the last packet in the operation */ if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { mOperationDone = true; + } else { + checkForSrm(); } return true; @@ -636,16 +705,7 @@ public final class ClientOperation implements Operation, BaseStream { if (mPrivateInput == null) { mPrivateInput = new PrivateInputStream(this); } - - if (!mGetFinalFlag) { - sendRequest(0x03); - } else { - sendRequest(0x83); - - if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { - mOperationDone = true; - } - } + sendRequest(ObexHelper.OBEX_OPCODE_GET); return true; } else if (mOperationDone) { @@ -653,12 +713,13 @@ public final class ClientOperation implements Operation, BaseStream { } } else { + // PUT operation if ((!inStream) && (!mOperationDone)) { // to deal with outputstream in put operation if (mReplyHeader.responseCode == -1) { mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; } - sendRequest(0x02); + sendRequest(ObexHelper.OBEX_OPCODE_PUT); return true; } else if ((inStream) && (!mOperationDone)) { // How to deal with inputstream in put operation ? @@ -696,7 +757,7 @@ public final class ClientOperation implements Operation, BaseStream { } while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { - more = sendRequest(0x02); + more = sendRequest(ObexHelper.OBEX_OPCODE_PUT); } /* @@ -706,7 +767,7 @@ public final class ClientOperation implements Operation, BaseStream { */ while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { - sendRequest(0x82); + sendRequest(ObexHelper.OBEX_OPCODE_PUT_FINAL); } mOperationDone = true; } else if ((inStream) && (mOperationDone)) { @@ -724,12 +785,14 @@ public final class ClientOperation implements Operation, BaseStream { } while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { - if (!sendRequest(0x83)) { + if (!sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL)) { break; } } while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { - mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput); + mParent.sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL, null, + mReplyHeader, mPrivateInput, false); + // Regardless of the SRM state, wait for the response. } mOperationDone = true; } else if ((!inStream) && (!mOperationDone)) { @@ -752,9 +815,9 @@ public final class ClientOperation implements Operation, BaseStream { mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { - more = sendRequest(0x03); + more = sendRequest(ObexHelper.OBEX_OPCODE_GET); } - sendRequest(0x83); + sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL); // parent.sendRequest(0x83, null, replyHeaders, privateInput); if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { mOperationDone = true; @@ -764,5 +827,6 @@ public final class ClientOperation implements Operation, BaseStream { } public void noBodyHeader(){ + mSendBodyHeader = false; } } diff --git a/obex/javax/obex/ClientSession.java b/obex/javax/obex/ClientSession.java index 27d8976..272a920 100644 --- a/obex/javax/obex/ClientSession.java +++ b/obex/javax/obex/ClientSession.java @@ -1,4 +1,6 @@ /* + * Copyright (c) 2015 The Android Open Source Project + * Copyright (C) 2015 Samsung LSI * Copyright (c) 2008-2009, Motorola, Inc. * * All rights reserved. @@ -37,12 +39,16 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import android.util.Log; + /** * This class in an implementation of the OBEX ClientSession. * @hide */ public final class ClientSession extends ObexSession { + private static final String TAG = "ClientSession"; + private boolean mOpen; // Determines if an OBEX layer connection has been established @@ -51,10 +57,10 @@ public final class ClientSession extends ObexSession { private byte[] mConnectionId = null; /* - * The max Packet size must be at least 256 according to the OBEX + * The max Packet size must be at least 255 according to the OBEX * specification. */ - private int maxPacketSize = 256; + private int mMaxTxPacketSize = ObexHelper.LOWER_LIMIT_MAX_PACKET_SIZE; private boolean mRequestActive; @@ -62,11 +68,33 @@ public final class ClientSession extends ObexSession { private final OutputStream mOutput; + private final boolean mLocalSrmSupported; + + private final ObexTransport mTransport; + public ClientSession(final ObexTransport trans) throws IOException { mInput = trans.openInputStream(); mOutput = trans.openOutputStream(); mOpen = true; mRequestActive = false; + mLocalSrmSupported = trans.isSrmSupported(); + mTransport = trans; + } + + /** + * Create a ClientSession + * @param trans The transport to use for OBEX transactions + * @param supportsSrm True if Single Response Mode should be used e.g. if the + * supplied transport is a TCP or l2cap channel. + * @throws IOException if it occurs while opening the transport streams. + */ + public ClientSession(final ObexTransport trans, final boolean supportsSrm) throws IOException { + mInput = trans.openInputStream(); + mOutput = trans.openOutputStream(); + mOpen = true; + mRequestActive = false; + mLocalSrmSupported = supportsSrm; + mTransport = trans; } public HeaderSet connect(final HeaderSet header) throws IOException { @@ -98,23 +126,25 @@ public final class ClientSession extends ObexSession { * Byte 7 to n: headers */ byte[] requestPacket = new byte[totalLength]; + int maxRxPacketSize = ObexHelper.getMaxRxPacketSize(mTransport); // We just need to start at byte 3 since the sendRequest() method will // handle the length and 0x80. requestPacket[0] = (byte)0x10; requestPacket[1] = (byte)0x00; - requestPacket[2] = (byte)(ObexHelper.MAX_PACKET_SIZE_INT >> 8); - requestPacket[3] = (byte)(ObexHelper.MAX_PACKET_SIZE_INT & 0xFF); + requestPacket[2] = (byte)(maxRxPacketSize >> 8); + requestPacket[3] = (byte)(maxRxPacketSize & 0xFF); if (head != null) { System.arraycopy(head, 0, requestPacket, 4, head.length); } - // check with local max packet size + // Since we are not yet connected, the peer max packet size is unknown, + // hence we are only guaranteed the server will use the first 7 bytes. if ((requestPacket.length + 3) > ObexHelper.MAX_PACKET_SIZE_INT) { - throw new IOException("Packet size exceeds max packet size"); + throw new IOException("Packet size exceeds max packet size for connect"); } HeaderSet returnHeaderSet = new HeaderSet(); - sendRequest(ObexHelper.OBEX_OPCODE_CONNECT, requestPacket, returnHeaderSet, null); + sendRequest(ObexHelper.OBEX_OPCODE_CONNECT, requestPacket, returnHeaderSet, null, false); /* * Read the response from the OBEX server. @@ -158,7 +188,18 @@ public final class ClientSession extends ObexSession { System.arraycopy(mConnectionId, 0, head.mConnectionID, 0, 4); } - return new ClientOperation(maxPacketSize, this, head, true); + if(mLocalSrmSupported) { + head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, ObexHelper.OBEX_SRM_ENABLE); + /* TODO: Consider creating an interface to get the wait state. + * On an android system, I cannot see when this is to be used. + * except perhaps if we are to wait for user accept on a push message. + if(getLocalWaitState()) { + head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, ObexHelper.OBEX_SRMP_WAIT); + } + */ + } + + return new ClientOperation(mMaxTxPacketSize, this, head, true); } /** @@ -202,7 +243,7 @@ public final class ClientSession extends ObexSession { } head = ObexHelper.createHeader(header, false); - if ((head.length + 3) > maxPacketSize) { + if ((head.length + 3) > mMaxTxPacketSize) { throw new IOException("Packet size exceeds max packet size"); } } else { @@ -215,7 +256,7 @@ public final class ClientSession extends ObexSession { } HeaderSet returnHeaderSet = new HeaderSet(); - sendRequest(ObexHelper.OBEX_OPCODE_DISCONNECT, head, returnHeaderSet, null); + sendRequest(ObexHelper.OBEX_OPCODE_DISCONNECT, head, returnHeaderSet, null, false); /* * An OBEX DISCONNECT reply from the server: @@ -269,7 +310,16 @@ public final class ClientSession extends ObexSession { System.arraycopy(mConnectionId, 0, head.mConnectionID, 0, 4); } - return new ClientOperation(maxPacketSize, this, head, false); + if(mLocalSrmSupported) { + head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, ObexHelper.OBEX_SRM_ENABLE); + /* TODO: Consider creating an interface to get the wait state. + * On an android system, I cannot see when this is to be used. + if(getLocalWaitState()) { + head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, ObexHelper.OBEX_SRMP_WAIT); + } + */ + } + return new ClientOperation(mMaxTxPacketSize, this, head, false); } public void setAuthenticator(Authenticator auth) throws IOException { @@ -314,7 +364,7 @@ public final class ClientSession extends ObexSession { head = ObexHelper.createHeader(headset, false); totalLength += head.length; - if (totalLength > maxPacketSize) { + if (totalLength > mMaxTxPacketSize) { throw new IOException("Packet size exceeds max packet size"); } @@ -348,7 +398,7 @@ public final class ClientSession extends ObexSession { } HeaderSet returnHeaderSet = new HeaderSet(); - sendRequest(ObexHelper.OBEX_OPCODE_SETPATH, packet, returnHeaderSet, null); + sendRequest(ObexHelper.OBEX_OPCODE_SETPATH, packet, returnHeaderSet, null, false); /* * An OBEX SETPATH reply from the server: @@ -400,20 +450,40 @@ public final class ClientSession extends ObexSession { * @param head the headers to send to the client * @param header the header object to update with the response * @param privateInput the input stream used by the Operation object; null - * if this is called on a CONNECT, SETPATH or DISCONNECT return + * if this is called on a CONNECT, SETPATH or DISCONNECT + * @return * <code>true</code> if the operation completed successfully; * <code>false</code> if an authentication response failed to pass * @throws IOException if an IO error occurs */ public boolean sendRequest(int opCode, byte[] head, HeaderSet header, - PrivateInputStream privateInput) throws IOException { + PrivateInputStream privateInput, boolean srmActive) throws IOException { //check header length with local max size if (head != null) { if ((head.length + 3) > ObexHelper.MAX_PACKET_SIZE_INT) { + // TODO: This is an implementation limit - not a specification requirement. throw new IOException("header too large "); } } + boolean skipSend = false; + boolean skipReceive = false; + if (srmActive == true) { + if (opCode == ObexHelper.OBEX_OPCODE_PUT) { + // we are in the middle of a SRM PUT operation, don't expect a continue. + skipReceive = true; + } else if (opCode == ObexHelper.OBEX_OPCODE_GET) { + // We are still sending the get request, send, but don't expect continue + // until the request is transfered (the final bit is set) + skipReceive = true; + } else if (opCode == ObexHelper.OBEX_OPCODE_GET_FINAL) { + // All done sending the request, expect data from the server, without + // sending continue. + skipSend = true; + } + + } + int bytesReceived; ByteArrayOutputStream out = new ByteArrayOutputStream(); out.write((byte)opCode); @@ -428,86 +498,105 @@ public final class ClientSession extends ObexSession { out.write(head); } - // Write the request to the output stream and flush the stream - mOutput.write(out.toByteArray()); - mOutput.flush(); + if (!skipSend) { + // Write the request to the output stream and flush the stream + mOutput.write(out.toByteArray()); + // TODO: is this really needed? if this flush is implemented + // correctly, we will get a gap between each obex packet. + // which is kind of the idea behind SRM to avoid. + // Consider offloading to another thread (async action) + mOutput.flush(); + } - header.responseCode = mInput.read(); + if (!skipReceive) { + header.responseCode = mInput.read(); - int length = ((mInput.read() << 8) | (mInput.read())); + int length = ((mInput.read() << 8) | (mInput.read())); - if (length > ObexHelper.MAX_PACKET_SIZE_INT) { - throw new IOException("Packet received exceeds packet size limit"); - } - if (length > ObexHelper.BASE_PACKET_LENGTH) { - byte[] data = null; - if (opCode == ObexHelper.OBEX_OPCODE_CONNECT) { - @SuppressWarnings("unused") - int version = mInput.read(); - @SuppressWarnings("unused") - int flags = mInput.read(); - maxPacketSize = (mInput.read() << 8) + mInput.read(); + if (length > ObexHelper.getMaxRxPacketSize(mTransport)) { + throw new IOException("Packet received exceeds packet size limit"); + } + if (length > ObexHelper.BASE_PACKET_LENGTH) { + byte[] data = null; + if (opCode == ObexHelper.OBEX_OPCODE_CONNECT) { + @SuppressWarnings("unused") + int version = mInput.read(); + @SuppressWarnings("unused") + int flags = mInput.read(); + mMaxTxPacketSize = (mInput.read() << 8) + mInput.read(); + + //check with local max size + if (mMaxTxPacketSize > ObexHelper.MAX_CLIENT_PACKET_SIZE) { + mMaxTxPacketSize = ObexHelper.MAX_CLIENT_PACKET_SIZE; + } - //check with local max size - if (maxPacketSize > ObexHelper.MAX_CLIENT_PACKET_SIZE) { - maxPacketSize = ObexHelper.MAX_CLIENT_PACKET_SIZE; - } + // check with transport maximum size + if(mMaxTxPacketSize > ObexHelper.getMaxTxPacketSize(mTransport)) { + // To increase this size, increase the buffer size in L2CAP layer + // in Bluedroid. + Log.w(TAG, "An OBEX packet size of " + mMaxTxPacketSize + "was" + + " requested. Transport only allows: " + + ObexHelper.getMaxTxPacketSize(mTransport) + + " Lowering limit to this value."); + mMaxTxPacketSize = ObexHelper.getMaxTxPacketSize(mTransport); + } - if (length > 7) { - data = new byte[length - 7]; + if (length > 7) { + data = new byte[length - 7]; - bytesReceived = mInput.read(data); - while (bytesReceived != (length - 7)) { - bytesReceived += mInput.read(data, bytesReceived, data.length - - bytesReceived); + bytesReceived = mInput.read(data); + while (bytesReceived != (length - 7)) { + bytesReceived += mInput.read(data, bytesReceived, data.length + - bytesReceived); + } + } else { + return true; } } else { - return true; - } - } else { - data = new byte[length - 3]; - bytesReceived = mInput.read(data); + data = new byte[length - 3]; + bytesReceived = mInput.read(data); - while (bytesReceived != (length - 3)) { - bytesReceived += mInput.read(data, bytesReceived, data.length - bytesReceived); - } - if (opCode == ObexHelper.OBEX_OPCODE_ABORT) { - return true; + while (bytesReceived != (length - 3)) { + bytesReceived += mInput.read(data, bytesReceived, data.length - bytesReceived); + } + if (opCode == ObexHelper.OBEX_OPCODE_ABORT) { + return true; + } } - } - byte[] body = ObexHelper.updateHeaderSet(header, data); - if ((privateInput != null) && (body != null)) { - privateInput.writeBytes(body, 1); - } + byte[] body = ObexHelper.updateHeaderSet(header, data); + if ((privateInput != null) && (body != null)) { + privateInput.writeBytes(body, 1); + } - if (header.mConnectionID != null) { - mConnectionId = new byte[4]; - System.arraycopy(header.mConnectionID, 0, mConnectionId, 0, 4); - } + if (header.mConnectionID != null) { + mConnectionId = new byte[4]; + System.arraycopy(header.mConnectionID, 0, mConnectionId, 0, 4); + } - if (header.mAuthResp != null) { - if (!handleAuthResp(header.mAuthResp)) { - setRequestInactive(); - throw new IOException("Authentication Failed"); + if (header.mAuthResp != null) { + if (!handleAuthResp(header.mAuthResp)) { + setRequestInactive(); + throw new IOException("Authentication Failed"); + } } - } - if ((header.responseCode == ResponseCodes.OBEX_HTTP_UNAUTHORIZED) - && (header.mAuthChall != null)) { + if ((header.responseCode == ResponseCodes.OBEX_HTTP_UNAUTHORIZED) + && (header.mAuthChall != null)) { - if (handleAuthChall(header)) { - out.write((byte)HeaderSet.AUTH_RESPONSE); - out.write((byte)((header.mAuthResp.length + 3) >> 8)); - out.write((byte)(header.mAuthResp.length + 3)); - out.write(header.mAuthResp); - header.mAuthChall = null; - header.mAuthResp = null; + if (handleAuthChall(header)) { + out.write((byte)HeaderSet.AUTH_RESPONSE); + out.write((byte)((header.mAuthResp.length + 3) >> 8)); + out.write((byte)(header.mAuthResp.length + 3)); + out.write(header.mAuthResp); + header.mAuthChall = null; + header.mAuthResp = null; - byte[] sendHeaders = new byte[out.size() - 3]; - System.arraycopy(out.toByteArray(), 3, sendHeaders, 0, sendHeaders.length); + byte[] sendHeaders = new byte[out.size() - 3]; + System.arraycopy(out.toByteArray(), 3, sendHeaders, 0, sendHeaders.length); - return sendRequest(opCode, sendHeaders, header, privateInput); + return sendRequest(opCode, sendHeaders, header, privateInput, false); + } } } } @@ -520,4 +609,8 @@ public final class ClientSession extends ObexSession { mInput.close(); mOutput.close(); } + + public boolean isSrmSupported() { + return mLocalSrmSupported; + } } diff --git a/obex/javax/obex/HeaderSet.java b/obex/javax/obex/HeaderSet.java index 51b560a..35fe186 100644 --- a/obex/javax/obex/HeaderSet.java +++ b/obex/javax/obex/HeaderSet.java @@ -40,7 +40,7 @@ import java.security.SecureRandom; /** * This class implements the javax.obex.HeaderSet interface for OBEX over - * RFCOMM. + * RFCOMM or OBEX over l2cap. * @hide */ public final class HeaderSet { @@ -178,6 +178,22 @@ public final class HeaderSet { */ public static final int OBJECT_CLASS = 0x4F; + /** + * Represents the OBEX Single Response Mode (SRM). This header is used + * for Single response mode, introduced in OBEX 1.5. + * <P> + * The value of <code>SINGLE_RESPONSE_MODE</code> is 0x97 (151). + */ + public static final int SINGLE_RESPONSE_MODE = 0x97; + + /** + * Represents the OBEX Single Response Mode Parameters. This header is used + * for Single response mode, introduced in OBEX 1.5. + * <P> + * The value of <code>SINGLE_RESPONSE_MODE_PARAMETER</code> is 0x98 (152). + */ + public static final int SINGLE_RESPONSE_MODE_PARAMETER = 0x98; + private Long mCount; // 4 byte unsigned integer private String mName; // null terminated Unicode text string @@ -204,7 +220,7 @@ public final class HeaderSet { private byte[] mObjectClass; // byte sequence - private String[] mUnicodeUserDefined; //null terminated unicode string + private String[] mUnicodeUserDefined; // null terminated unicode string private byte[][] mSequenceUserDefined; // byte sequence user defined @@ -212,7 +228,12 @@ public final class HeaderSet { private Long[] mIntegerUserDefined; // 4 byte unsigned integer - private final SecureRandom mRandom; + private SecureRandom mRandom = null; + + private Byte mSingleResponseMode; // byte to indicate enable/disable/support for SRM + + private Byte mSrmParam; // byte representing the SRM parameters - only "wait" + // is supported by Bluetooth /*package*/ byte[] nonce; @@ -234,7 +255,6 @@ public final class HeaderSet { mByteUserDefined = new Byte[16]; mIntegerUserDefined = new Long[16]; responseCode = -1; - mRandom = new SecureRandom(); } /** @@ -393,6 +413,30 @@ public final class HeaderSet { } } break; + case SINGLE_RESPONSE_MODE: + if (headerValue == null) { + mSingleResponseMode = null; + } else { + if (!(headerValue instanceof Byte)) { + throw new IllegalArgumentException( + "Single Response Mode must be a Byte"); + } else { + mSingleResponseMode = (Byte)headerValue; + } + } + break; + case SINGLE_RESPONSE_MODE_PARAMETER: + if (headerValue == null) { + mSrmParam = null; + } else { + if (!(headerValue instanceof Byte)) { + throw new IllegalArgumentException( + "Single Response Mode Parameter must be a Byte"); + } else { + mSrmParam = (Byte)headerValue; + } + } + break; default: // Verify that it was not a Unicode String user Defined if ((headerID >= 0x30) && (headerID <= 0x3F)) { @@ -493,6 +537,10 @@ public final class HeaderSet { return mObjectClass; case APPLICATION_PARAMETER: return mAppParam; + case SINGLE_RESPONSE_MODE: + return mSingleResponseMode; + case SINGLE_RESPONSE_MODE_PARAMETER: + return mSrmParam; default: // Verify that it was not a Unicode String user Defined if ((headerID >= 0x30) && (headerID <= 0x3F)) { @@ -564,6 +612,12 @@ public final class HeaderSet { if (mObjectClass != null) { out.write(OBJECT_CLASS); } + if(mSingleResponseMode != null) { + out.write(SINGLE_RESPONSE_MODE); + } + if(mSrmParam != null) { + out.write(SINGLE_RESPONSE_MODE_PARAMETER); + } for (int i = 0x30; i < 0x40; i++) { if (mUnicodeUserDefined[i - 0x30] != null) { @@ -625,6 +679,9 @@ public final class HeaderSet { throws IOException { nonce = new byte[16]; + if(mRandom == null) { + mRandom = new SecureRandom(); + } for (int i = 0; i < 16; i++) { nonce[i] = (byte)mRandom.nextInt(); } diff --git a/obex/javax/obex/ObexHelper.java b/obex/javax/obex/ObexHelper.java index 0a06709..fa50943 100644 --- a/obex/javax/obex/ObexHelper.java +++ b/obex/javax/obex/ObexHelper.java @@ -1,5 +1,6 @@ /* - * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2015 Samsung LSI * Copyright (c) 2008-2009, Motorola, Inc. * * All rights reserved. @@ -42,12 +43,16 @@ import java.util.Calendar; import java.util.Date; import java.util.TimeZone; +import android.util.Log; + /** * This class defines a set of helper methods for the implementation of Obex. * @hide */ public final class ObexHelper { + private static final String TAG = "ObexHelper"; + public static final boolean VDBG = false; /** * Defines the basic packet length used by OBEX. Every OBEX packet has the * same basic format:<BR> @@ -65,18 +70,24 @@ public final class ObexHelper { * should be the Max incoming MTU minus TODO: L2CAP package headers and * RFCOMM package headers. TODO: Retrieve the max incoming MTU from TODO: * LocalDevice.getProperty(). + * NOTE: This value must be larger than or equal to the L2CAP SDU */ /* * android note set as 0xFFFE to match remote MPS */ public static final int MAX_PACKET_SIZE_INT = 0xFFFE; + // The minimum allowed max packet size is 255 according to the OBEX specification + public static final int LOWER_LIMIT_MAX_PACKET_SIZE = 255; + /** * Temporary workaround to be able to push files to Windows 7. * TODO: Should be removed as soon as Microsoft updates their driver. */ public static final int MAX_CLIENT_PACKET_SIZE = 0xFC00; + public static final int OBEX_OPCODE_FINAL_BIT_MASK = 0x80; + public static final int OBEX_OPCODE_CONNECT = 0x80; public static final int OBEX_OPCODE_DISCONNECT = 0x81; @@ -119,6 +130,12 @@ public final class ObexHelper { public static final int OBEX_AUTH_REALM_CHARSET_UNICODE = 0xFF; + public static final byte OBEX_SRM_ENABLE = 0x01; // For BT we only need enable/disable + public static final byte OBEX_SRM_DISABLE = 0x00; + public static final byte OBEX_SRM_SUPPORT = 0x02; // Unused for now + + public static final byte OBEX_SRMP_WAIT = 0x01; // Only SRMP value used by BT + /** * Updates the HeaderSet with the headers received in the byte array * provided. Invalid headers are ignored. @@ -314,7 +331,7 @@ public final class ObexHelper { } } catch (Exception e) { // Not a valid header so ignore - throw new IOException("Header was not formatted properly"); + throw new IOException("Header was not formatted properly", e); } index += 4; break; @@ -322,7 +339,7 @@ public final class ObexHelper { } } catch (IOException e) { - throw new IOException("Header was not formatted properly"); + throw new IOException("Header was not formatted properly", e); } return body; @@ -672,6 +689,33 @@ public final class ObexHelper { } } + // TODO: + // If the SRM and SRMP header is in use, they must be send in the same OBEX packet + // But the current structure of the obex code cannot handle this, and therefore + // it makes sense to put them in the tail of the headers, since we then reduce the + // chance of enabling SRM to soon. The down side is that SRM cannot be used while + // transferring non-body headers + + // Add the SRM header + byteHeader = (Byte)headImpl.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if (byteHeader != null) { + out.write((byte)HeaderSet.SINGLE_RESPONSE_MODE); + out.write(byteHeader.byteValue()); + if (nullOut) { + headImpl.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, null); + } + } + + // Add the SRM parameter header + byteHeader = (Byte)headImpl.getHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + if (byteHeader != null) { + out.write((byte)HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + out.write(byteHeader.byteValue()); + if (nullOut) { + headImpl.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, null); + } + } + } catch (IOException e) { } finally { result = out.toByteArray(); @@ -702,6 +746,8 @@ public final class ObexHelper { int index = start; int length = 0; + // TODO: Ensure SRM and SRMP headers are not split into two OBEX packets + while ((fullLength < maxSize) && (index < headerArray.length)) { int headerID = (headerArray[index] < 0 ? headerArray[index] + 256 : headerArray[index]); lastLength = fullLength; @@ -1008,4 +1054,39 @@ public final class ObexHelper { return authChall; } + + /** + * Return the maximum allowed OBEX packet to transmit. + * OBEX packets transmitted must be smaller than this value. + * @param transport Reference to the ObexTransport in use. + * @return the maximum allowed OBEX packet to transmit + */ + public static int getMaxTxPacketSize(ObexTransport transport) { + int size = transport.getMaxTransmitPacketSize(); + return validateMaxPacketSize(size); + } + + /** + * Return the maximum allowed OBEX packet to receive - used in OBEX connect. + * @param transport + * @return he maximum allowed OBEX packet to receive + */ + public static int getMaxRxPacketSize(ObexTransport transport) { + int size = transport.getMaxReceivePacketSize(); + return validateMaxPacketSize(size); + } + + private static int validateMaxPacketSize(int size) { + if(VDBG && (size > MAX_PACKET_SIZE_INT)) Log.w(TAG, + "The packet size supported for the connection (" + size + ") is larger" + + " than the configured OBEX packet size: " + MAX_PACKET_SIZE_INT); + if(size != -1) { + if(size < LOWER_LIMIT_MAX_PACKET_SIZE) { + throw new IllegalArgumentException(size + " is less that the lower limit: " + + LOWER_LIMIT_MAX_PACKET_SIZE); + } + return size; + } + return MAX_PACKET_SIZE_INT; + } } diff --git a/obex/javax/obex/ObexPacket.java b/obex/javax/obex/ObexPacket.java new file mode 100644 index 0000000..bb6c96e --- /dev/null +++ b/obex/javax/obex/ObexPacket.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2015 The Android Open Source Project + * Copyright (c) 2015 Samsung LSI + * + * 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 javax.obex; + +import java.io.IOException; +import java.io.InputStream; + +public class ObexPacket { + public int mHeaderId; + public int mLength; + public byte[] mPayload = null; + + private ObexPacket(int headerId, int length) { + mHeaderId = headerId; + mLength = length; + } + + /** + * Create a complete OBEX packet by reading data from an InputStream. + * @param is the input stream to read from. + * @return the OBEX packet read. + * @throws IOException if an IO exception occurs during read. + */ + public static ObexPacket read(InputStream is) throws IOException { + int headerId = is.read(); + return read(headerId, is); + } + + /** + * Read the remainder of an OBEX packet, with a specified headerId. + * @param headerId the headerId already read from the stream. + * @param is the stream to read from, assuming 1 byte have already been read. + * @return the OBEX packet read. + * @throws IOException + */ + public static ObexPacket read(int headerId, InputStream is) throws IOException { + // Read the 2 byte length field from the stream + int length = is.read(); + length = (length << 8) + is.read(); + + ObexPacket newPacket = new ObexPacket(headerId, length); + + int bytesReceived; + byte[] temp = null; + if (length > 3) { + // First three bytes already read, compensating for this + temp = new byte[length - 3]; + bytesReceived = is.read(temp); + while (bytesReceived != temp.length) { + bytesReceived += is.read(temp, bytesReceived, temp.length - bytesReceived); + } + } + newPacket.mPayload = temp; + return newPacket; + } +} diff --git a/obex/javax/obex/ObexSession.java b/obex/javax/obex/ObexSession.java index a7daeb5..542b9c8 100644 --- a/obex/javax/obex/ObexSession.java +++ b/obex/javax/obex/ObexSession.java @@ -34,6 +34,8 @@ package javax.obex; import java.io.IOException; +import android.util.Log; + /** * The <code>ObexSession</code> interface characterizes the term * "OBEX Connection" as defined in the IrDA Object Exchange Protocol v1.2, which @@ -47,6 +49,9 @@ import java.io.IOException; */ public class ObexSession { + private static final String TAG = "ObexSession"; + private static final boolean V = ObexHelper.VDBG; + protected Authenticator mAuthenticator; protected byte[] mChallengeDigest; @@ -125,6 +130,7 @@ public class ObexSession { result = mAuthenticator .onAuthenticationChallenge(realm, isUserIDRequired, isFullAccess); } catch (Exception e) { + if (V) Log.d(TAG, "Exception occured - returning false", e); return false; } diff --git a/obex/javax/obex/ObexTransport.java b/obex/javax/obex/ObexTransport.java index 445e267..a5a75f5 100644 --- a/obex/javax/obex/ObexTransport.java +++ b/obex/javax/obex/ObexTransport.java @@ -73,4 +73,39 @@ public interface ObexTransport { DataOutputStream openDataOutputStream() throws IOException; + /** + * Must return the maximum allowed OBEX packet that can be sent over + * the transport. For L2CAP this will be the Max SDU reported by the + * peer device. + * The returned value will be used to set the outgoing OBEX packet + * size. Therefore this value shall not change. + * For RFCOMM or other transport types where the OBEX packets size + * is unrelated to the transport packet size, return -1; + * @return the maximum allowed OBEX packet that can be send over + * the transport. Or -1 in case of don't care. + */ + int getMaxTransmitPacketSize(); + + /** + * Must return the maximum allowed OBEX packet that can be received over + * the transport. For L2CAP this will be the Max SDU configured for the + * L2CAP channel. + * The returned value will be used to validate the incoming packet size + * values. + * For RFCOMM or other transport types where the OBEX packets size + * is unrelated to the transport packet size, return -1; + * @return the maximum allowed OBEX packet that can be send over + * the transport. Or -1 in case of don't care. + */ + int getMaxReceivePacketSize(); + + /** + * Shall return true if the transport in use supports SRM. + * @return + * <code>true</code> if SRM operation is supported, and is to be enabled. + * <code>false</code> if SRM operations are not supported, or should not be used. + */ + boolean isSrmSupported(); + + } diff --git a/obex/javax/obex/ServerOperation.java b/obex/javax/obex/ServerOperation.java index fc441e0..56a675a 100644 --- a/obex/javax/obex/ServerOperation.java +++ b/obex/javax/obex/ServerOperation.java @@ -1,4 +1,5 @@ -/* +/* Copyright (c) 2015 The Android Open Source Project + * Copyright (C) 2015 Samsung LSI * Copyright (c) 2008-2009, Motorola, Inc. * * All rights reserved. @@ -39,6 +40,8 @@ import java.io.OutputStream; import java.io.DataOutputStream; import java.io.ByteArrayOutputStream; +import android.util.Log; + /** * This class implements the Operation interface for server side connections. * <P> @@ -54,6 +57,10 @@ import java.io.ByteArrayOutputStream; */ public final class ServerOperation implements Operation, BaseStream { + private static final String TAG = "ServerOperation"; + + private static final boolean V = ObexHelper.VDBG; // Verbose debugging + public boolean isAborted; public HeaderSet requestHeader; @@ -78,6 +85,8 @@ public final class ServerOperation implements Operation, BaseStream { private PrivateOutputStream mPrivateOutput; + private ObexTransport mTransport; + private boolean mPrivateOutputOpen; private String mExceptionString; @@ -89,6 +98,19 @@ public final class ServerOperation implements Operation, BaseStream { private boolean mHasBody; private boolean mSendBodyHeader = true; + // Assume SRM disabled - needs to be explicit + // enabled by client + private boolean mSrmEnabled = false; + // A latch - when triggered, there is not way back ;-) + private boolean mSrmActive = false; + // Set to true when a SRM enable response have been send + private boolean mSrmResponseSent = false; + // keep waiting until final-bit is received in request + // to handle the case where the SRM enable header is in + // a different OBEX packet than the SRMP header. + private boolean mSrmWaitingForRemote = true; + // Why should we wait? - currently not exposed to apps. + private boolean mSrmLocalWait = false; /** * Creates new ServerOperation @@ -116,12 +138,14 @@ public final class ServerOperation implements Operation, BaseStream { mRequestFinished = false; mPrivateOutputOpen = false; mHasBody = false; - int bytesReceived; + ObexPacket packet; + mTransport = p.getTransport(); /* * Determine if this is a PUT request */ - if ((request == 0x02) || (request == 0x82)) { + if ((request == ObexHelper.OBEX_OPCODE_PUT) || + (request == ObexHelper.OBEX_OPCODE_PUT_FINAL)) { /* * It is a PUT request. */ @@ -130,13 +154,14 @@ public final class ServerOperation implements Operation, BaseStream { /* * Determine if the final bit is set */ - if ((request & 0x80) == 0) { + if ((request & ObexHelper.OBEX_OPCODE_FINAL_BIT_MASK) == 0) { finalBitSet = false; } else { finalBitSet = true; mRequestFinished = true; } - } else if ((request == 0x03) || (request == 0x83)) { + } else if ((request == ObexHelper.OBEX_OPCODE_GET) || + (request == ObexHelper.OBEX_OPCODE_GET_FINAL)) { /* * It is a GET request. */ @@ -145,71 +170,32 @@ public final class ServerOperation implements Operation, BaseStream { // For Get request, final bit set is decided by server side logic finalBitSet = false; - if (request == 0x83) { + if (request == ObexHelper.OBEX_OPCODE_GET_FINAL) { mRequestFinished = true; } } else { throw new IOException("ServerOperation can not handle such request"); } - int length = in.read(); - length = (length << 8) + in.read(); + packet = ObexPacket.read(request, mInput); /* * Determine if the packet length is larger than this device can receive */ - if (length > ObexHelper.MAX_PACKET_SIZE_INT) { + if (packet.mLength > ObexHelper.getMaxRxPacketSize(mTransport)) { mParent.sendResponse(ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE, null); - throw new IOException("Packet received was too large"); + throw new IOException("Packet received was too large. Length: " + + packet.mLength + " maxLength: " + ObexHelper.getMaxRxPacketSize(mTransport)); } /* * Determine if any headers were sent in the initial request */ - if (length > 3) { - byte[] data = new byte[length - 3]; - bytesReceived = in.read(data); - - while (bytesReceived != data.length) { - bytesReceived += in.read(data, bytesReceived, data.length - bytesReceived); + if (packet.mLength > 3) { + if(!handleObexPacket(packet)) { + return; } - - byte[] body = ObexHelper.updateHeaderSet(requestHeader, data); - - if (body != null) { - mHasBody = true; - } - - if (mListener.getConnectionId() != -1 && requestHeader.mConnectionID != null) { - mListener.setConnectionId(ObexHelper.convertToLong(requestHeader.mConnectionID)); - } else { - mListener.setConnectionId(1); - } - - if (requestHeader.mAuthResp != null) { - if (!mParent.handleAuthResp(requestHeader.mAuthResp)) { - mExceptionString = "Authentication Failed"; - mParent.sendResponse(ResponseCodes.OBEX_HTTP_UNAUTHORIZED, null); - mClosed = true; - requestHeader.mAuthResp = null; - return; - } - } - - if (requestHeader.mAuthChall != null) { - mParent.handleAuthChall(requestHeader); - // send the authResp to the client - replyHeader.mAuthResp = new byte[requestHeader.mAuthResp.length]; - System.arraycopy(requestHeader.mAuthResp, 0, replyHeader.mAuthResp, 0, - replyHeader.mAuthResp.length); - requestHeader.mAuthResp = null; - requestHeader.mAuthChall = null; - - } - - if (body != null) { - mPrivateInput.writeBytes(body, 1); - } else { + if (!mHasBody) { while ((!mGetOperation) && (!finalBitSet)) { sendReply(ResponseCodes.OBEX_HTTP_CONTINUE); if (mPrivateInput.available() > 0) { @@ -232,6 +218,100 @@ public final class ServerOperation implements Operation, BaseStream { } } + /** + * Parse headers and update member variables + * @param packet the received obex packet + * @return false for failing authentication - and a OBEX_HTTP_UNAUTHORIZED + * response have been send. Else true. + * @throws IOException + */ + private boolean handleObexPacket(ObexPacket packet) throws IOException { + byte[] body = updateRequestHeaders(packet); + + if (body != null) { + mHasBody = true; + } + if (mListener.getConnectionId() != -1 && requestHeader.mConnectionID != null) { + mListener.setConnectionId(ObexHelper + .convertToLong(requestHeader.mConnectionID)); + } else { + mListener.setConnectionId(1); + } + + if (requestHeader.mAuthResp != null) { + if (!mParent.handleAuthResp(requestHeader.mAuthResp)) { + mExceptionString = "Authentication Failed"; + mParent.sendResponse(ResponseCodes.OBEX_HTTP_UNAUTHORIZED, null); + mClosed = true; + requestHeader.mAuthResp = null; + return false; + } + requestHeader.mAuthResp = null; + } + + if (requestHeader.mAuthChall != null) { + mParent.handleAuthChall(requestHeader); + // send the auhtResp to the client + replyHeader.mAuthResp = new byte[requestHeader.mAuthResp.length]; + System.arraycopy(requestHeader.mAuthResp, 0, replyHeader.mAuthResp, 0, + replyHeader.mAuthResp.length); + requestHeader.mAuthResp = null; + requestHeader.mAuthChall = null; + } + + if (body != null) { + mPrivateInput.writeBytes(body, 1); + } + return true; + } + + /** + * Update the request header set, and sniff on SRM headers to update local state. + * @param data the OBEX packet data + * @return any bytes in a body/end-of-body header returned by {@link ObexHelper.updateHeaderSet} + * @throws IOException + */ + private byte[] updateRequestHeaders(ObexPacket packet) throws IOException { + byte[] body = null; + if (packet.mPayload != null) { + body = ObexHelper.updateHeaderSet(requestHeader, packet.mPayload); + } + Byte srmMode = (Byte)requestHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if(mTransport.isSrmSupported() && srmMode != null + && srmMode == ObexHelper.OBEX_SRM_ENABLE) { + mSrmEnabled = true; + if(V) Log.d(TAG,"SRM is now ENABLED (but not active) for this operation"); + } + checkForSrmWait(packet.mHeaderId); + if((!mSrmWaitingForRemote) && (mSrmEnabled)) { + if(V) Log.d(TAG,"SRM is now ACTIVE for this operation"); + mSrmActive = true; + } + return body; + } + + /** + * Call this only when a complete request have been received. + * (This is not optimal, but the current design is not really suited to + * the way SRM is specified.) + */ + private void checkForSrmWait(int headerId){ + if (mSrmEnabled && (headerId == ObexHelper.OBEX_OPCODE_GET + || headerId == ObexHelper.OBEX_OPCODE_GET_FINAL + || headerId == ObexHelper.OBEX_OPCODE_PUT)) { + try { + mSrmWaitingForRemote = false; + Byte srmp = (Byte)requestHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + if(srmp != null && srmp == ObexHelper.OBEX_SRMP_WAIT) { + mSrmWaitingForRemote = true; + // Clear the wait header, as the absents of the header when the final bit is set + // indicates don't wait. + requestHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, null); + } + } catch (IOException e) {if(V){Log.w(TAG,"Exception while extracting header",e);}} + } + } + public boolean isValidBody() { return mHasBody; } @@ -274,17 +354,19 @@ public final class ServerOperation implements Operation, BaseStream { /** * Sends a reply to the client. If the reply is a OBEX_HTTP_CONTINUE, it - * will wait for a response from the client before ending. + * will wait for a response from the client before ending unless SRM is active. * @param type the response code to send back to the client * @return <code>true</code> if the final bit was not set on the reply; * <code>false</code> if no reply was received because the operation - * ended, an abort was received, or the final bit was set in the - * reply + * ended, an abort was received, the final bit was set in the + * reply or SRM is active. * @throws IOException if an IO error occurs */ public synchronized boolean sendReply(int type) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); - int bytesReceived; + boolean skipSend = false; + boolean skipReceive = false; + boolean srmRespSendPending = false; long id = mListener.getConnectionId(); if (id == -1) { @@ -293,7 +375,19 @@ public final class ServerOperation implements Operation, BaseStream { replyHeader.mConnectionID = ObexHelper.convertToByteArray(id); } - byte[] headerArray = ObexHelper.createHeader(replyHeader, true); + if(mSrmEnabled && !mSrmResponseSent) { + // As we are not ensured that the SRM enable is in the first OBEX packet + // We must check for each reply. + if(V)Log.v(TAG, "mSrmEnabled==true, sending SRM enable response."); + replyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, (byte)ObexHelper.OBEX_SRM_ENABLE); + srmRespSendPending = true; + } + + if(mSrmEnabled && !mGetOperation && mSrmLocalWait) { + replyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, (byte)ObexHelper.OBEX_SRMP_WAIT); + } + + byte[] headerArray = ObexHelper.createHeader(replyHeader, true); // This clears the headers int bodyLength = -1; int orginalBodyLength = -1; @@ -347,6 +441,28 @@ public final class ServerOperation implements Operation, BaseStream { finalBitSet = true; } + if(mSrmActive) { + if(!mGetOperation && type == ResponseCodes.OBEX_HTTP_CONTINUE && + mSrmResponseSent == true) { + // we are in the middle of a SRM PUT operation, don't send a continue. + skipSend = true; + } else if(mGetOperation && mRequestFinished == false && mSrmResponseSent == true) { + // We are still receiving the get request, receive, but don't send continue. + skipSend = true; + } else if(mGetOperation && mRequestFinished == true) { + // All done receiving the GET request, send data to the client, without + // expecting a continue. + skipReceive = true; + } + if(V)Log.v(TAG, "type==" + type + " skipSend==" + skipSend + + " skipReceive==" + skipReceive); + } + if(srmRespSendPending) { + if(V)Log.v(TAG, + "SRM Enabled (srmRespSendPending == true)- sending SRM Enable response"); + mSrmResponseSent = true; + } + if ((finalBitSet) || (headerArray.length < (mMaxPacketLength - 20))) { if (bodyLength > 0) { /* @@ -387,7 +503,7 @@ public final class ServerOperation implements Operation, BaseStream { } if ((finalBitSet) && (type == ResponseCodes.OBEX_HTTP_OK) && (orginalBodyLength <= 0)) { - if(mSendBodyHeader == true) { + if(mSendBodyHeader) { out.write(0x49); orginalBodyLength = 3; out.write((byte)(orginalBodyLength >> 8)); @@ -395,107 +511,66 @@ public final class ServerOperation implements Operation, BaseStream { } } - mResponseSize = 3; - mParent.sendResponse(type, out.toByteArray()); + if(skipSend == false) { + mResponseSize = 3; + mParent.sendResponse(type, out.toByteArray()); + } if (type == ResponseCodes.OBEX_HTTP_CONTINUE) { - int headerID = mInput.read(); - int length = mInput.read(); - length = (length << 8) + mInput.read(); - if ((headerID != ObexHelper.OBEX_OPCODE_PUT) - && (headerID != ObexHelper.OBEX_OPCODE_PUT_FINAL) - && (headerID != ObexHelper.OBEX_OPCODE_GET) - && (headerID != ObexHelper.OBEX_OPCODE_GET_FINAL)) { - - if (length > 3) { - byte[] temp = new byte[length - 3]; - // First three bytes already read, compensating for this - bytesReceived = mInput.read(temp); - - while (bytesReceived != temp.length) { - bytesReceived += mInput.read(temp, bytesReceived, - temp.length - bytesReceived); - } - } - /* - * Determine if an ABORT was sent as the reply - */ - if (headerID == ObexHelper.OBEX_OPCODE_ABORT) { - mParent.sendResponse(ResponseCodes.OBEX_HTTP_OK, null); - mClosed = true; - isAborted = true; - mExceptionString = "Abort Received"; - throw new IOException("Abort Received"); - } else { - mParent.sendResponse(ResponseCodes.OBEX_HTTP_BAD_REQUEST, null); - mClosed = true; - mExceptionString = "Bad Request Received"; - throw new IOException("Bad Request Received"); - } + if(mGetOperation && skipReceive) { + // Here we need to check for and handle abort (throw an exception). + // Any other signal received should be discarded silently (only on server side) + checkSrmRemoteAbort(); } else { - - if ((headerID == ObexHelper.OBEX_OPCODE_PUT_FINAL)) { - finalBitSet = true; - } else if (headerID == ObexHelper.OBEX_OPCODE_GET_FINAL) { - mRequestFinished = true; - } - - /* - * Determine if the packet length is larger then this device can receive - */ - if (length > ObexHelper.MAX_PACKET_SIZE_INT) { - mParent.sendResponse(ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE, null); - throw new IOException("Packet received was too large"); - } - - /* - * Determine if any headers were sent in the initial request - */ - if (length > 3) { - byte[] data = new byte[length - 3]; - bytesReceived = mInput.read(data); - - while (bytesReceived != data.length) { - bytesReceived += mInput.read(data, bytesReceived, data.length - - bytesReceived); - } - byte[] body = ObexHelper.updateHeaderSet(requestHeader, data); - if (body != null) { - mHasBody = true; - } - if (mListener.getConnectionId() != -1 && requestHeader.mConnectionID != null) { - mListener.setConnectionId(ObexHelper - .convertToLong(requestHeader.mConnectionID)); + // Receive and handle data (only send reply if !skipSend) + // Read a complete OBEX Packet + ObexPacket packet = ObexPacket.read(mInput); + + int headerId = packet.mHeaderId; + if ((headerId != ObexHelper.OBEX_OPCODE_PUT) + && (headerId != ObexHelper.OBEX_OPCODE_PUT_FINAL) + && (headerId != ObexHelper.OBEX_OPCODE_GET) + && (headerId != ObexHelper.OBEX_OPCODE_GET_FINAL)) { + + /* + * Determine if an ABORT was sent as the reply + */ + if (headerId == ObexHelper.OBEX_OPCODE_ABORT) { + handleRemoteAbort(); } else { - mListener.setConnectionId(1); + // TODO:shall we send this if it occurs during SRM? Errata on the subject + mParent.sendResponse(ResponseCodes.OBEX_HTTP_BAD_REQUEST, null); + mClosed = true; + mExceptionString = "Bad Request Received"; + throw new IOException("Bad Request Received"); } + } else { - if (requestHeader.mAuthResp != null) { - if (!mParent.handleAuthResp(requestHeader.mAuthResp)) { - mExceptionString = "Authentication Failed"; - mParent.sendResponse(ResponseCodes.OBEX_HTTP_UNAUTHORIZED, null); - mClosed = true; - requestHeader.mAuthResp = null; - return false; - } - requestHeader.mAuthResp = null; + if ((headerId == ObexHelper.OBEX_OPCODE_PUT_FINAL)) { + finalBitSet = true; + } else if (headerId == ObexHelper.OBEX_OPCODE_GET_FINAL) { + mRequestFinished = true; } - if (requestHeader.mAuthChall != null) { - mParent.handleAuthChall(requestHeader); - // send the auhtResp to the client - replyHeader.mAuthResp = new byte[requestHeader.mAuthResp.length]; - System.arraycopy(requestHeader.mAuthResp, 0, replyHeader.mAuthResp, 0, - replyHeader.mAuthResp.length); - requestHeader.mAuthResp = null; - requestHeader.mAuthChall = null; + /* + * Determine if the packet length is larger than the negotiated packet size + */ + if (packet.mLength > ObexHelper.getMaxRxPacketSize(mTransport)) { + mParent.sendResponse(ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE, null); + throw new IOException("Packet received was too large"); } - if (body != null) { - mPrivateInput.writeBytes(body, 1); + /* + * Determine if any headers were sent in the initial request + */ + if (packet.mLength > 3 || (mSrmEnabled && packet.mLength == 3)) { + if(handleObexPacket(packet) == false) { + return false; + } } } + } return true; } else { @@ -504,6 +579,53 @@ public final class ServerOperation implements Operation, BaseStream { } /** + * This method will look for an abort from the peer during a SRM transfer. + * The function will not block if no data has been received from the remote device. + * If data have been received, the function will block while reading the incoming + * OBEX package. + * An Abort request will be handled, and cause an IOException("Abort Received"). + * Other messages will be discarded silently as per GOEP specification. + * @throws IOException if an abort request have been received. + * TODO: I think this is an error in the specification. If we discard other messages, + * the peer device will most likely stall, as it will not receive the expected + * response for the message... + * I'm not sure how to understand "Receipt of invalid or unexpected SRM or SRMP + * header values shall be ignored by the receiving device." + * If any signal is received during an active SRM transfer it is unexpected regardless + * whether or not it contains SRM/SRMP headers... + */ + private void checkSrmRemoteAbort() throws IOException { + if(mInput.available() > 0) { + ObexPacket packet = ObexPacket.read(mInput); + /* + * Determine if an ABORT was sent as the reply + */ + if (packet.mHeaderId == ObexHelper.OBEX_OPCODE_ABORT) { + handleRemoteAbort(); + } else { + // TODO: should we throw an exception here anyway? - don't see how to + // ignore SRM/SRMP headers without ignoring the complete signal + // (in this particular case). + Log.w(TAG, "Received unexpected request from client - discarding...\n" + + " headerId: " + packet.mHeaderId + " length: " + packet.mLength); + } + } + } + + private void handleRemoteAbort() throws IOException { + /* TODO: To increase the speed of the abort operation in SRM, we need + * to be able to flush the L2CAP queue for the PSM in use. + * This could be implemented by introducing a control + * message to be send over the socket, that in the abort case + * could carry a flush command. */ + mParent.sendResponse(ResponseCodes.OBEX_HTTP_OK, null); + mClosed = true; + isAborted = true; + mExceptionString = "Abort Received"; + throw new IOException("Abort Received"); + } + + /** * Sends an ABORT message to the server. By calling this method, the * corresponding input and output streams will be closed along with this * object. diff --git a/obex/javax/obex/ServerRequestHandler.java b/obex/javax/obex/ServerRequestHandler.java index 0882572..09cbc2c 100644 --- a/obex/javax/obex/ServerRequestHandler.java +++ b/obex/javax/obex/ServerRequestHandler.java @@ -275,4 +275,13 @@ public class ServerRequestHandler { */ public void onClose() { } + + /** + * Override to add Single Response Mode support - e.g. if the supplied + * transport is l2cap. + * @return True if SRM is supported, else False + */ + public boolean isSrmSupported() { + return false; + } } diff --git a/obex/javax/obex/ServerSession.java b/obex/javax/obex/ServerSession.java index f1b9a0d..acee5dd 100644 --- a/obex/javax/obex/ServerSession.java +++ b/obex/javax/obex/ServerSession.java @@ -1,4 +1,6 @@ /* + * Copyright (C) 2015 The Android Open Source Project + * Copyright (c) 2015 Samsung LSI * Copyright (c) 2008-2009, Motorola, Inc. * * All rights reserved. @@ -45,6 +47,7 @@ import java.io.OutputStream; public final class ServerSession extends ObexSession implements Runnable { private static final String TAG = "Obex ServerSession"; + private static final boolean V = ObexHelper.VDBG; private ObexTransport mTransport; @@ -91,7 +94,9 @@ public final class ServerSession extends ObexSession implements Runnable { boolean done = false; while (!done && !mClosed) { + if(V) Log.v(TAG, "Waiting for incoming request..."); int requestType = mInput.read(); + if(V) Log.v(TAG, "Read request: " + requestType); switch (requestType) { case ObexHelper.OBEX_OPCODE_CONNECT: handleConnectRequest(); @@ -140,9 +145,9 @@ public final class ServerSession extends ObexSession implements Runnable { } } catch (NullPointerException e) { - Log.d(TAG, e.toString()); + Log.d(TAG, "Exception occured - ignoring", e); } catch (Exception e) { - Log.d(TAG, e.toString()); + Log.d(TAG, "Exception occured - ignoring", e); } close(); } @@ -163,7 +168,7 @@ public final class ServerSession extends ObexSession implements Runnable { int length = mInput.read(); length = (length << 8) + mInput.read(); - if (length > ObexHelper.MAX_PACKET_SIZE_INT) { + if (length > ObexHelper.getMaxRxPacketSize(mTransport)) { code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; } else { for (int i = 3; i < length; i++) { @@ -215,6 +220,7 @@ public final class ServerSession extends ObexSession implements Runnable { *internal error should not be sent because server has already replied with *OK response in "sendReply") */ + if(V) Log.d(TAG,"Exception occured - sending OBEX_HTTP_INTERNAL_ERROR reply",e); if (!op.isAborted) { sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); } @@ -243,6 +249,7 @@ public final class ServerSession extends ObexSession implements Runnable { op.sendReply(response); } } catch (Exception e) { + if(V) Log.d(TAG,"Exception occured - sending OBEX_HTTP_INTERNAL_ERROR reply",e); sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); } } @@ -275,7 +282,7 @@ public final class ServerSession extends ObexSession implements Runnable { data[2] = (byte)totalLength; } op.write(data); - op.flush(); + op.flush(); // TODO: Do we need to flush? } /** @@ -304,7 +311,7 @@ public final class ServerSession extends ObexSession implements Runnable { flags = mInput.read(); constants = mInput.read(); - if (length > ObexHelper.MAX_PACKET_SIZE_INT) { + if (length > ObexHelper.getMaxRxPacketSize(mTransport)) { code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; totalLength = 3; } else { @@ -358,6 +365,7 @@ public final class ServerSession extends ObexSession implements Runnable { try { code = mListener.onSetPath(request, reply, backup, create); } catch (Exception e) { + if(V) Log.d(TAG,"Exception occured - sending OBEX_HTTP_INTERNAL_ERROR reply",e); sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); return; } @@ -425,7 +433,7 @@ public final class ServerSession extends ObexSession implements Runnable { length = mInput.read(); length = (length << 8) + mInput.read(); - if (length > ObexHelper.MAX_PACKET_SIZE_INT) { + if (length > ObexHelper.getMaxRxPacketSize(mTransport)) { code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; totalLength = 3; } else { @@ -466,6 +474,7 @@ public final class ServerSession extends ObexSession implements Runnable { try { mListener.onDisconnect(request, reply); } catch (Exception e) { + if(V) Log.d(TAG,"Exception occured - sending OBEX_HTTP_INTERNAL_ERROR reply",e); sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); return; } @@ -531,23 +540,38 @@ public final class ServerSession extends ObexSession implements Runnable { HeaderSet reply = new HeaderSet(); int bytesReceived; + if(V) Log.v(TAG,"handleConnectRequest()"); + /* * Read in the length of the OBEX packet, OBEX version, flags, and max * packet length */ packetLength = mInput.read(); packetLength = (packetLength << 8) + mInput.read(); + if(V) Log.v(TAG,"handleConnectRequest() - packetLength: " + packetLength); + version = mInput.read(); flags = mInput.read(); mMaxPacketLength = mInput.read(); mMaxPacketLength = (mMaxPacketLength << 8) + mInput.read(); + if(V) Log.v(TAG,"handleConnectRequest() - version: " + version + + " MaxLength: " + mMaxPacketLength + " flags: " + flags); + // should we check it? if (mMaxPacketLength > ObexHelper.MAX_PACKET_SIZE_INT) { mMaxPacketLength = ObexHelper.MAX_PACKET_SIZE_INT; } - if (packetLength > ObexHelper.MAX_PACKET_SIZE_INT) { + if(mMaxPacketLength > ObexHelper.getMaxTxPacketSize(mTransport)) { + Log.w(TAG, "Requested MaxObexPacketSize " + mMaxPacketLength + + " is larger than the max size supported by the transport: " + + ObexHelper.getMaxTxPacketSize(mTransport) + + " Reducing to this size."); + mMaxPacketLength = ObexHelper.getMaxTxPacketSize(mTransport); + } + + if (packetLength > ObexHelper.getMaxRxPacketSize(mTransport)) { code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; totalLength = 7; } else { @@ -614,7 +638,7 @@ public final class ServerSession extends ObexSession implements Runnable { code = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } } catch (Exception e) { - e.printStackTrace(); + if(V) Log.d(TAG,"Exception occured - sending OBEX_HTTP_INTERNAL_ERROR reply",e); totalLength = 7; head = null; code = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; @@ -633,13 +657,14 @@ public final class ServerSession extends ObexSession implements Runnable { * Packet Length (Defined in MAX_PACKET_SIZE) Byte 7 to n: headers */ byte[] sendData = new byte[totalLength]; + int maxRxLength = ObexHelper.getMaxRxPacketSize(mTransport); sendData[0] = (byte)code; sendData[1] = length[2]; sendData[2] = length[3]; sendData[3] = (byte)0x10; sendData[4] = (byte)0x00; - sendData[5] = (byte)(ObexHelper.MAX_PACKET_SIZE_INT >> 8); - sendData[6] = (byte)(ObexHelper.MAX_PACKET_SIZE_INT & 0xFF); + sendData[5] = (byte)(maxRxLength >> 8); + sendData[6] = (byte)(maxRxLength & 0xFF); if (head != null) { System.arraycopy(head, 0, sendData, 7, head.length); @@ -659,11 +684,16 @@ public final class ServerSession extends ObexSession implements Runnable { mListener.onClose(); } try { - mInput.close(); - mOutput.close(); - mTransport.close(); + /* Set state to closed before interrupting the thread by closing the streams */ mClosed = true; + if(mInput != null) + mInput.close(); + if(mOutput != null) + mOutput.close(); + if(mTransport != null) + mTransport.close(); } catch (Exception e) { + if(V) Log.d(TAG,"Exception occured during close() - ignore",e); } mTransport = null; mInput = null; @@ -702,4 +732,7 @@ public final class ServerSession extends ObexSession implements Runnable { return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } + public ObexTransport getTransport() { + return mTransport; + } } diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index b5796c9..484908d 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -19,6 +19,7 @@ package com.android.server; import static android.Manifest.permission.RECEIVE_DATA_ACTIVITY_CHANGE; import static android.net.ConnectivityManager.CONNECTIVITY_ACTION; import static android.net.ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE; +import static android.net.ConnectivityManager.NETID_UNSET; import static android.net.ConnectivityManager.TYPE_NONE; import static android.net.ConnectivityManager.TYPE_VPN; import static android.net.ConnectivityManager.getNetworkTypeName; @@ -89,6 +90,7 @@ import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.Xml; @@ -709,16 +711,15 @@ public class ConnectivityService extends IConnectivityManager.Stub return mNextNetworkRequestId++; } - private void assignNextNetId(NetworkAgentInfo nai) { + private int reserveNetId() { synchronized (mNetworkForNetId) { for (int i = MIN_NET_ID; i <= MAX_NET_ID; i++) { int netId = mNextNetId; if (++mNextNetId > MAX_NET_ID) mNextNetId = MIN_NET_ID; // Make sure NetID unused. http://b/16815182 - if (mNetworkForNetId.get(netId) == null) { - nai.network = new Network(netId); - mNetworkForNetId.put(netId, nai); - return; + if (!mNetIdInUse.get(netId)) { + mNetIdInUse.put(netId, true); + return netId; } } } @@ -739,7 +740,9 @@ public class ConnectivityService extends IConnectivityManager.Stub info = new NetworkInfo(nai.networkInfo); lp = new LinkProperties(nai.linkProperties); nc = new NetworkCapabilities(nai.networkCapabilities); - network = new Network(nai.network); + // Network objects are outwardly immutable so there is no point to duplicating. + // Duplicating also precludes sharing socket factories and connection pools. + network = nai.network; subscriberId = (nai.networkMisc != null) ? nai.networkMisc.subscriberId : null; } info.setType(networkType); @@ -807,7 +810,9 @@ public class ConnectivityService extends IConnectivityManager.Stub info = new NetworkInfo(nai.networkInfo); lp = new LinkProperties(nai.linkProperties); nc = new NetworkCapabilities(nai.networkCapabilities); - network = new Network(nai.network); + // Network objects are outwardly immutable so there is no point to duplicating. + // Duplicating also precludes sharing socket factories and connection pools. + network = nai.network; subscriberId = (nai.networkMisc != null) ? nai.networkMisc.subscriberId : null; } } @@ -873,6 +878,28 @@ public class ConnectivityService extends IConnectivityManager.Stub return getFilteredNetworkInfo(state.networkInfo, state.linkProperties, uid); } + @Override + public Network getActiveNetwork() { + enforceAccessPermission(); + final int uid = Binder.getCallingUid(); + final int user = UserHandle.getUserId(uid); + int vpnNetId = NETID_UNSET; + synchronized (mVpns) { + final Vpn vpn = mVpns.get(user); + if (vpn != null && vpn.appliesToUid(uid)) vpnNetId = vpn.getNetId(); + } + NetworkAgentInfo nai; + if (vpnNetId != NETID_UNSET) { + synchronized (mNetworkForNetId) { + nai = mNetworkForNetId.get(vpnNetId); + } + if (nai != null) return nai.network; + } + nai = getDefaultNetwork(); + if (nai != null && isNetworkWithLinkPropertiesBlocked(nai.linkProperties, uid)) nai = null; + return nai != null ? nai.network : null; + } + /** * Find the first Provisioning network. * @@ -985,13 +1012,13 @@ public class ConnectivityService extends IConnectivityManager.Stub @Override public Network[] getAllNetworks() { enforceAccessPermission(); - final ArrayList<Network> result = new ArrayList(); synchronized (mNetworkForNetId) { + final Network[] result = new Network[mNetworkForNetId.size()]; for (int i = 0; i < mNetworkForNetId.size(); i++) { - result.add(new Network(mNetworkForNetId.valueAt(i).network)); + result[i] = mNetworkForNetId.valueAt(i).network; } + return result; } - return result.toArray(new Network[result.size()]); } private NetworkCapabilities getNetworkCapabilitiesAndValidation(NetworkAgentInfo nai) { @@ -1960,6 +1987,7 @@ public class ConnectivityService extends IConnectivityManager.Stub if (nai != null) { synchronized (mNetworkForNetId) { mNetworkForNetId.remove(nai.network.netId); + mNetIdInUse.delete(nai.network.netId); } // Just in case. mLegacyTypeTracker.remove(nai); @@ -2003,6 +2031,7 @@ public class ConnectivityService extends IConnectivityManager.Stub mLegacyTypeTracker.remove(nai); synchronized (mNetworkForNetId) { mNetworkForNetId.remove(nai.network.netId); + mNetIdInUse.delete(nai.network.netId); } // Since we've lost the network, go through all the requests that // it was satisfying and see if any other factory can satisfy them. @@ -2549,25 +2578,27 @@ public class ConnectivityService extends IConnectivityManager.Stub public void reportInetCondition(int networkType, int percentage) { NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType); if (nai == null) return; - boolean isGood = percentage > 50; - // Revalidate if the app report does not match our current validated state. - if (isGood != nai.lastValidated) { - // Make the message logged by reportBadNetwork below less confusing. - if (DBG && isGood) log("reportInetCondition: type=" + networkType + " ok, revalidate"); - reportBadNetwork(nai.network); - } + reportNetworkConnectivity(nai.network, percentage > 50); } - public void reportBadNetwork(Network network) { + public void reportNetworkConnectivity(Network network, boolean hasConnectivity) { enforceAccessPermission(); enforceInternetPermission(); - if (network == null) return; - - final int uid = Binder.getCallingUid(); - NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network); + NetworkAgentInfo nai; + if (network == null) { + nai = getDefaultNetwork(); + } else { + nai = getNetworkAgentInfoForNetwork(network); + } if (nai == null) return; - if (DBG) log("reportBadNetwork(" + nai.name() + ") by " + uid); + // Revalidate if the app report does not match our current validated state. + if (hasConnectivity == nai.lastValidated) return; + final int uid = Binder.getCallingUid(); + if (DBG) { + log("reportNetworkConnectivity(" + nai.network.netId + ", " + hasConnectivity + + ") by " + uid); + } synchronized (nai) { // Validating an uncreated network could result in a call to rematchNetworkAndRequests() // which isn't meant to work on uncreated networks. @@ -3026,23 +3057,6 @@ public class ConnectivityService extends IConnectivityManager.Stub } } - public int findConnectionTypeForIface(String iface) { - enforceConnectivityInternalPermission(); - - if (TextUtils.isEmpty(iface)) return ConnectivityManager.TYPE_NONE; - - synchronized(mNetworkForNetId) { - for (int i = 0; i < mNetworkForNetId.size(); i++) { - NetworkAgentInfo nai = mNetworkForNetId.valueAt(i); - LinkProperties lp = nai.linkProperties; - if (lp != null && iface.equals(lp.getInterfaceName()) && nai.networkInfo != null) { - return nai.networkInfo.getType(); - } - } - } - return ConnectivityManager.TYPE_NONE; - } - @Override public int checkMobileProvisioning(int suggestedTimeOutMs) { // TODO: Remove? Any reason to trigger a provisioning check? @@ -3296,7 +3310,7 @@ public class ConnectivityService extends IConnectivityManager.Stub loge("Starting user already has a VPN"); return; } - userVpn = new Vpn(mHandler.getLooper(), mContext, mNetd, this, userId); + userVpn = new Vpn(mHandler.getLooper(), mContext, mNetd, userId); mVpns.put(userId, userVpn); } } @@ -3548,14 +3562,23 @@ public class ConnectivityService extends IConnectivityManager.Stub * and the are the highest scored network available. * the are keyed off the Requests requestId. */ + // TODO: Yikes, this is accessed on multiple threads: add synchronization. private final SparseArray<NetworkAgentInfo> mNetworkForRequestId = new SparseArray<NetworkAgentInfo>(); + // NOTE: Accessed on multiple threads, must be synchronized on itself. + @GuardedBy("mNetworkForNetId") private final SparseArray<NetworkAgentInfo> mNetworkForNetId = new SparseArray<NetworkAgentInfo>(); + // NOTE: Accessed on multiple threads, synchronized with mNetworkForNetId. + // An entry is first added to mNetIdInUse, prior to mNetworkForNetId, so + // there may not be a strict 1:1 correlation between the two. + @GuardedBy("mNetworkForNetId") + private final SparseBooleanArray mNetIdInUse = new SparseBooleanArray(); // NetworkAgentInfo keyed off its connecting messenger // TODO - eval if we can reduce the number of lists/hashmaps/sparsearrays + // NOTE: Only should be accessed on ConnectivityServiceThread, except dump(). private final HashMap<Messenger, NetworkAgentInfo> mNetworkAgentInfos = new HashMap<Messenger, NetworkAgentInfo>(); @@ -3570,7 +3593,7 @@ public class ConnectivityService extends IConnectivityManager.Stub return nai == getDefaultNetwork(); } - public void registerNetworkAgent(Messenger messenger, NetworkInfo networkInfo, + public int registerNetworkAgent(Messenger messenger, NetworkInfo networkInfo, LinkProperties linkProperties, NetworkCapabilities networkCapabilities, int currentScore, NetworkMisc networkMisc) { enforceConnectivityInternalPermission(); @@ -3578,20 +3601,23 @@ public class ConnectivityService extends IConnectivityManager.Stub // TODO: Instead of passing mDefaultRequest, provide an API to determine whether a Network // satisfies mDefaultRequest. NetworkAgentInfo nai = new NetworkAgentInfo(messenger, new AsyncChannel(), - new NetworkInfo(networkInfo), new LinkProperties(linkProperties), - new NetworkCapabilities(networkCapabilities), currentScore, mContext, mTrackerHandler, - new NetworkMisc(networkMisc), mDefaultRequest); + new Network(reserveNetId()), new NetworkInfo(networkInfo), new LinkProperties( + linkProperties), new NetworkCapabilities(networkCapabilities), currentScore, + mContext, mTrackerHandler, new NetworkMisc(networkMisc), mDefaultRequest); synchronized (this) { nai.networkMonitor.systemReady = mSystemReady; } if (DBG) log("registerNetworkAgent " + nai); mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_AGENT, nai)); + return nai.network.netId; } private void handleRegisterNetworkAgent(NetworkAgentInfo na) { if (VDBG) log("Got NetworkAgent Messenger"); mNetworkAgentInfos.put(na.messenger, na); - assignNextNetId(na); + synchronized (mNetworkForNetId) { + mNetworkForNetId.put(na.network.netId, na); + } na.asyncChannel.connect(mContext, mTrackerHandler, na.messenger); NetworkInfo networkInfo = na.networkInfo; na.networkInfo = null; diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java index dac0580..8a7c902 100644 --- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java +++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java @@ -40,7 +40,10 @@ import java.util.ArrayList; */ public class NetworkAgentInfo { public NetworkInfo networkInfo; - public Network network; + // This Network object should always be used if possible, so as to encourage reuse of the + // enclosed socket factory and connection pool. Avoid creating other Network objects. + // This Network object is always valid. + public final Network network; public LinkProperties linkProperties; public NetworkCapabilities networkCapabilities; public final NetworkMonitor networkMonitor; @@ -86,12 +89,12 @@ public class NetworkAgentInfo { // Used by ConnectivityService to keep track of 464xlat. public Nat464Xlat clatd; - public NetworkAgentInfo(Messenger messenger, AsyncChannel ac, NetworkInfo info, + public NetworkAgentInfo(Messenger messenger, AsyncChannel ac, Network net, NetworkInfo info, LinkProperties lp, NetworkCapabilities nc, int score, Context context, Handler handler, NetworkMisc misc, NetworkRequest defaultRequest) { this.messenger = messenger; asyncChannel = ac; - network = null; + network = net; networkInfo = info; linkProperties = lp; networkCapabilities = nc; diff --git a/services/core/java/com/android/server/connectivity/NetworkMonitor.java b/services/core/java/com/android/server/connectivity/NetworkMonitor.java index d0e1665..7e20276 100644 --- a/services/core/java/com/android/server/connectivity/NetworkMonitor.java +++ b/services/core/java/com/android/server/connectivity/NetworkMonitor.java @@ -216,7 +216,7 @@ public class NetworkMonitor extends StateMachine { // If a network is not validated, make one attempt every 10 mins to see if it starts working. private static final int REEVALUATE_PAUSE_MS = 10*60*1000; private static final int PERIODIC_ATTEMPTS = 1; - // When an application calls reportBadNetwork, only make one attempt. + // When an application calls reportNetworkConnectivity, only make one attempt. private static final int REEVALUATE_ATTEMPTS = 1; private final int mReevaluateDelayMs; private int mReevaluateToken = 0; diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 0b430ea..3d478f9 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -17,8 +17,13 @@ package com.android.server.connectivity; import static android.Manifest.permission.BIND_VPN_SERVICE; +import static android.net.ConnectivityManager.NETID_UNSET; import static android.net.RouteInfo.RTN_THROW; import static android.net.RouteInfo.RTN_UNREACHABLE; +import static android.os.UserHandle.PER_USER_RANGE; +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.AF_INET6; + import android.Manifest; import android.app.AppGlobals; import android.app.AppOpsManager; @@ -29,6 +34,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; @@ -64,6 +70,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.security.Credentials; import android.security.KeyStore; +import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.GuardedBy; @@ -110,7 +117,6 @@ public class Vpn { private LegacyVpnRunner mLegacyVpnRunner; private PendingIntent mStatusIntent; private volatile boolean mEnableTeardown = true; - private final IConnectivityManager mConnService; private final INetworkManagementService mNetd; private VpnConfig mConfig; private NetworkAgent mNetworkAgent; @@ -126,10 +132,9 @@ public class Vpn { private final int mUserHandle; public Vpn(Looper looper, Context context, INetworkManagementService netService, - IConnectivityManager connService, int userHandle) { + int userHandle) { mContext = context; mNetd = netService; - mConnService = connService; mUserHandle = userHandle; mLooper = looper; @@ -336,6 +341,10 @@ public class Vpn { return mNetworkInfo; } + public int getNetId() { + return mNetworkAgent != null ? mNetworkAgent.netId : NETID_UNSET; + } + private LinkProperties makeLinkProperties() { boolean allowIPv4 = mConfig.allowIPv4; boolean allowIPv6 = mConfig.allowIPv6; @@ -1106,11 +1115,15 @@ public class Vpn { // registering mOuterInterface = mConfig.interfaze; - try { - mOuterConnection.set( - mConnService.findConnectionTypeForIface(mOuterInterface)); - } catch (Exception e) { - mOuterConnection.set(ConnectivityManager.TYPE_NONE); + if (!TextUtils.isEmpty(mOuterInterface)) { + final ConnectivityManager cm = ConnectivityManager.from(mContext); + for (Network network : cm.getAllNetworks()) { + final LinkProperties lp = cm.getLinkProperties(network); + if (lp != null && mOuterInterface.equals(lp.getInterfaceName())) { + final NetworkInfo networkInfo = cm.getNetworkInfo(network); + if (networkInfo != null) mOuterConnection.set(networkInfo.getType()); + } + } } IntentFilter filter = new IntentFilter(); diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java index 719dd76..33a7fe1 100644 --- a/telecomm/java/android/telecom/Call.java +++ b/telecomm/java/android/telecom/Call.java @@ -203,14 +203,21 @@ public final class Call { */ public static final int CAPABILITY_SPEED_UP_MT_AUDIO = 0x00040000; - /** - * Call type can be modified for IMS call + /** + * Call can be upgraded to a video call. * @hide */ public static final int CAPABILITY_CAN_UPGRADE_TO_VIDEO = 0x00080000; + /** + * For video calls, indicates whether the outgoing video for the call can be paused using + * the {@link android.telecom.VideoProfile.VideoState#PAUSED} VideoState. + * @hide + */ + public static final int CAPABILITY_CAN_PAUSE_VIDEO = 0x00100000; + //****************************************************************************************** - // Next CAPABILITY value: 0x00100000 + // Next CAPABILITY value: 0x00200000 //****************************************************************************************** private final Uri mHandle; @@ -315,6 +322,9 @@ public final class Call { if (can(capabilities, CAPABILITY_CAN_UPGRADE_TO_VIDEO)) { builder.append(" CAPABILITY_CAN_UPGRADE_TO_VIDEO"); } + if (can(capabilities, CAPABILITY_CAN_PAUSE_VIDEO)) { + builder.append(" CAPABILITY_CAN_PAUSE_VIDEO"); + } builder.append("]"); return builder.toString(); } diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java index 3a54b1c..4762031 100644 --- a/telecomm/java/android/telecom/Connection.java +++ b/telecomm/java/android/telecom/Connection.java @@ -184,13 +184,20 @@ public abstract class Connection implements IConferenceable { public static final int CAPABILITY_SPEED_UP_MT_AUDIO = 0x00040000; /** - * Call type can be modified for IMS call + * Call can be upgraded to a video call. * @hide */ public static final int CAPABILITY_CAN_UPGRADE_TO_VIDEO = 0x00080000; + /** + * For video calls, indicates whether the outgoing video for the call can be paused using + * the {@link android.telecom.VideoProfile.VideoState#PAUSED} VideoState. + * @hide + */ + public static final int CAPABILITY_CAN_PAUSE_VIDEO = 0x00100000; + //********************************************************************************************** - // Next CAPABILITY value: 0x00100000 + // Next CAPABILITY value: 0x00200000 //********************************************************************************************** /** @@ -342,6 +349,9 @@ public abstract class Connection implements IConferenceable { if (can(capabilities, CAPABILITY_CAN_UPGRADE_TO_VIDEO)) { builder.append(" CAPABILITY_CAN_UPGRADE_TO_VIDEO"); } + if (can(capabilities, CAPABILITY_CAN_PAUSE_VIDEO)) { + builder.append(" CAPABILITY_CAN_PAUSE_VIDEO"); + } builder.append("]"); return builder.toString(); } |