diff --git a/.gitignore b/.gitignore index b0fb80e95..6a541764f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,9 @@ java/midp/javax/microedition/lcdui/MMHelperImpl.java java/midp/javax/wireless/messaging/MessageConnection.java java/midp/com/sun/amms/DirectSoundSource3D.java java/midp/com/sun/amms/GlobalMgrImpl.java +java/midp/com/sun/jsr082/bluetooth/BCC.java +java/midp/com/sun/jsr082/bluetooth/JavacallBluetoothStack.java +java/midp/com/sun/jsr082/bluetooth/SDDB.java +java/midp/com/sun/jsr082/bluetooth/ServiceDiscovererFactory.java +java/midp/com/sun/jsr082/bluetooth/btl2cap/L2CAPNotifierImpl.java +java/midp/com/sun/jsr082/bluetooth/btspp/BTSPPNotifierImpl.java diff --git a/Makefile b/Makefile index 4d0f39a18..c43e43dd6 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ test: all python tests/echoServer.py & cd tests && python httpsServer.py & cd tests && python sslEchoServer.py & + cd tests && python waitServers.py casperjs --engine=slimerjs test `pwd`/tests/automation.js > test.log killall python Python || true python dumplog.py diff --git a/README.md b/README.md index 04c8e947f..5e06a3c02 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ j2me.js is a J2ME virtual machine in JavaScript. The current goals of j2me.js are: 1. Run MIDlets in a way that emulates the reference implementation of phone ME Feature MR4 (b01) -1. Keep j2me.js simple and small: Leverage the phoneME JDK/infrastructre and existing Java code as much as we can, and implement as little as possible in JavaScript +1. Keep j2me.js simple and small: Leverage the phoneME JDK/infrastructure and existing Java code as much as we can, and implement as little as possible in JavaScript ## Building j2me.js @@ -20,41 +20,94 @@ Build using make: cd j2me.js make -## Running MIDlets, tests +## Running apps & MIDlets, Debugging -To run a MIDlet: +index.html is a webapp that runs j2me.js. The URL parameters you pass to index.html control the specific behavior of j2me.js. + +### URL parameters + +See full list at libs/urlparams.js + +* `main` - default is `com/sun/midp/main/MIDletSuiteLoader` +* `midletClassName` - must be set to the main class to run. Only valid when default `main` parameter is used. Defaults to `RunTests` +* `autosize` - if set to `1`, j2me app will fill the page. +* `gamepad` - if set to `1`, gamepad will be visible/available. + +### Desktop + +To run a MIDlet on desktop, you must first start an http server that will host index.html. You can then connect to the http server, passing URL parameters to index.html python tests/httpServer.py & http://localhost:8000/index.html?jad=ExampleApp.jad&jars=ExampleApp.jar&midletClassName=com.example.yourClassNameHere -URL parameters: +Example - Asteroids + + python tests/httpServer.py & + http://localhost:8000/index.html?midletClassName=asteroids.Game&jars=tests/tests.jar&gamepad=1 + +Some apps require access to APIs that aren't enabled by default on Desktop Firefox and there is no UI built in to Desktop Firefox to enable them. APIs matching this description include: + +* mozTCPSocket +* mozContacts +* mozbrowser +* notifications + +To enable this type of API for a MIDlet you're running, use [Myk's API Enabler Addon](https://github.com/mykmelez/tcpsocketpup) + +### FirefoxOS device (or emulator) + +To run a MIDlet on a FirefoxOS device, update the `launch_path` property in manifest.webapp. The `midletClassName` URL parameter needs to point to an app. + +Once you've updated manifest.webapp, connect to the device or emulator as described in the [FirefoxOS Developer Phone Guide](https://developer.mozilla.org/en-US/Firefox_OS/Developer_phone_guide/Flame) and select your j2me.js directory (the one containing manifest.webapp) when choosing the app to push to device. + +Example - Asteroids + + "launch_path": "/index.html?midletClassName=asteroids.Game&jars=tests/tests.jar&logConsole=web&autosize=1&gamepad=1" + +## Tests + +You can run the test suite with `make test`. The main driver for the test suite is automation.js which uses the Casper.js testing framework and slimer.js (a Gecko backend for casper.js). This test suite runs on every push (continuous integration) thanks to Travis. + +If you want to pass additional [casperJS command line options](http://docs.slimerjs.org/current/configuration.html), look at the "test" target in Makefile and place additional command line options before the automation.js filename. -* See full list at libs/urlparams.js -* Default `midletClassName` if none specified is `RunTests` +gfx tests use image comparison; a reference image is provided with the test and the output of the test must match the reference image. The output is allowed to differ from the reference image by a number of pixels specified in automation.js. -If testing sockets, 4 servers are necessary: +The main set of unit tests that automation.js runs is the set covered by the RunTests MIDlet. The full list of RunTests tests available in the tests/Testlets.java generated file. RunTests runs a number of "Testlets" (Java classes that implement the `Testlet` interface). + +### Running a single test + +If the test you want to run is a class with a main method, specify a `main` URL parameter to index.html, e.g.: + + main=gnu/testlet/vm/SystemTest&jars=tests/tests.jar + +If the test you want to run is a MIDlet, specify `midletClassName` and `jad` URL parameters to index.html (`main` will default to the MIDletSuiteLoader), e.g.: + + midletClassName=tests/alarm/MIDlet1&jad=tests/midlets/alarm/alarm.jad&jars=tests/tests.jar + +If the test you want to run is a Testlet , specify an `args` URL parameter to index.html. You can specify multiple Testlets separated by commas, and you can use either '.' or '/' in the class name, e.g.: + + args=java.lang.TestSystem,javax.crypto.TestRC4,com/nokia/mid/ui/TestVirtualKeyboard + +If the testlet uses sockets, you must start 4 servers (instead of just the http server): python tests/httpServer.py & python tests/echoServer.py & cd tests && python httpsServer.py & cd tests && python sslEchoServer.py & -To run specific tests (e.g. TestSystem and TestRC4): - - python tests/httpServer.py & - http://localhost:8000/index.html?args=java/lang/TestSystem,javax/crypto/TestRC4 +### Failures (and what to do) -Full list of RunTests tests available in the tests/Testlets.java generated file +Frequent causes of failure include: -There's no UI to enable certain privileges on Desktop that MIDlets might need. Use [Myk's tcpsocketpup addon](https://github.com/mykmelez/tcpsocketpup) to accomplish that +* automation.js expects a different number of tests to have passed than the number that actually passed (this is very common when adding new tests) +* timeout: Travis machines are generally slower than dev machines and so tests that pass locally will fail in the continuous integration tests +* Number of differing pixels in a gfx/rendering test exceeds the threshold allowed in automation.js. This will often happen because slimerJS uses a different version of Firefox than the developer. This can also happen because the test renders text, whose font rendering can vary from machine to machine, perhaps even with the same font. -Continuous integration: +gfx/rendering tests will print a number next to the error message. That number is the number of differing pixels. If it is close to the threshold you can probably just increase the threshold in automation.js with no ill effect. -* Uses Travis -* Runs automation.js which uses Casper.js testing framework and slimer.js (Gecko backend for casper.js) -* `make test` +The test output will include base64 encoded images; copy this into your browser's URL bar as a data URL to see what the actual test output looked like. -gfx tests use image comparison; output should match reference +When running `make test`, a file called `test.log` gets generated. Check that for additional info on the failures that occurred. ## Logging @@ -66,3 +119,94 @@ See `logConsole` and `logLevel` URL params in libs/console.js 1. Execute the jsshell.js file as follows: js jsshell.js package.ClassName + +## Coding Style + +In general, stick with whatever style exists in the file you are modifying. + +If you're creating a new file, use 4-space indents for Java and 2-space indents of JS. + +Use JavaDoc to document public APIs in Java. + +Modeline for Java files: + + /* vim: set filetype=java shiftwidth=4 tabstop=4 autoindent cindent expandtab : */ + +Modelines for JS files: + + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +## Profiling + +### JS profiling + +One way to profile j2me.js is to use the JS profiler available in Firefox Dev Tools. This will tell us how well the JVM is working and how well natives work. This type of profiling will not measure time that is taken waiting for async callbacks to be called (for example, when using the native JS filesystem API). + +When debugging using the WebIDE, enable the option "select an iframe as the currently targeted document" and select the iframe containing main.html as the debugging target. NB: you need to connect to a target device running FxOS 2.1 or up to use this feature in WebIDE. + +Use these JS calls within the console to start and stop profiling (TODO: I haven't actually gotten these to work): + + Instrument.startProfile() + Instrument.stopProfile() + +It can be helpful to increase this `about:config` option: `devtools.hud.loglimit.console` + +Alternatively, use the "Performance" tab of the Firefox Developer Tools. + +### Java profiling + +j2me.js includes its own profiler that is capable of measuring the performance of Java methods running inside its JVM. + +When running j2me.js in Desktop Firefox, click the "profile" button that appears below the output iframe. Press the button again to stop profiling. You should get output including the total time taken inside each method and the number of times each method was called. + +Add "&profile=1" to your URL parameter list to enable profile immediately upon loading j2me.js (index.html). + +## Filesystem + +midp/fs.js contains native implementations of various midp filesystem APIs. + +Those implementations call out to lib/fs.js which is a JS implementation of a filesystem. + +Uses async\_storage.js (from gaia) - async API for accessing IndexedDB + +Java APIs are sync but our implementations use async APIs + +## Implementing Java functions in native code + +`native` keyword tells Java that the function is implemented in native code + +e.g.: + + public static native long currentTimeMillis(); + +Java compiler will do nothing to ensure that implementation actually exists. At runtime, implementation better be available or you'll get a runtime exception. + +We use `Native` object in JS to handle creation and registration of `native` functions. See native.js + + Native.create("name/of/function.(parameterTypes)returnType", jsFuncToCall, isAsync) + +e.g.: + + Native.create("java/lang/System.arraycopy.(Ljava/lang/Object;ILjava/lang/Object;II)V", function(src, srcOffset, dst, dstOffset, length) {...}); + +If you need to implement a method in JS but you can't declare it `native` in Java, use `Override`. + +e.g.: + + Override.create("com/ibm/oti/connection/file/Connection.decode.(Ljava/lang/String;)Ljava/lang/String;", function(...) {...}); + + +If raising a Java `Exception`, throw new instance of Java `Exception` class + +e.g.: + + throw new JavaException("java/lang/NullPointerException", "Cannot copy to/from a null array."); + +Remember: + + * Return types are automatically converted to Java types, but parameters are not automatically converted from Java types to JS types + * Pass `true` as last param if JS will make async calls and return a `Promise` + * `this` will be available in any context that `this` would be available to the Java method. i.e. `this` will be `null` for `static` methods. + * Context is last param to every function registered using `Native.create` or `Override.create` + * Parameter types are specified in [JNI](http://www.iastate.edu/~java/docs/guide/nativemethod/types.doc.html) diff --git a/index.js b/index.js index c4d995e2c..7b3dc333e 100644 --- a/index.js +++ b/index.js @@ -36,10 +36,6 @@ var DumbPipe = { // Functions that receive messages from the other side for active pipes. recipients: {}, - // Queue of messages to send to the other side. Retrieved by the other side - // via a "get" message. - outgoingMessages: [], - // Every time we want to make the other side retrieve messages, the hash // of the other side's web page has to change, so we increment it. nextHashID: 0, @@ -76,9 +72,6 @@ var DumbPipe = { //console.log("outer recv: " + JSON.stringify(envelope)); this.receiveMessage(envelope.pipeID, envelope.message); break; - case "get": - this.getMessages(event); - break; case "close": //console.log("outer recv: " + JSON.stringify(envelope)); this.closePipe(envelope.pipeID); @@ -109,12 +102,12 @@ var DumbPipe = { // Oh my shod, that's some funky git! var envelope = { pipeID: pipeID, message: message }; //console.log("outer send: " + JSON.stringify(envelope)); - this.outgoingMessages.push(envelope); - var mozbrowser = document.getElementById("mozbrowser"); - window.setZeroTimeout(function() { - mozbrowser.src = mozbrowser.src.split("#")[0] + "#" + this.nextHashID++; - }.bind(this)); + try { + document.getElementById("mozbrowser").contentWindow.postMessage(envelope, "*"); + } catch (e) { + console.log("Error " + e + " while sending message: " + JSON.stringify(message)); + } }, receiveMessage: function(pipeID, message, detail) { @@ -132,17 +125,6 @@ var DumbPipe = { }.bind(this)); }, - getMessages: function(event) { - try { - event.detail.returnValue = JSON.stringify(this.outgoingMessages); - } catch(ex) { - console.error("failed to stringify outgoing messages: " + ex); - } finally { - this.outgoingMessages = []; - event.detail.unblock(); - } - }, - closePipe: function(pipeID) { delete this.recipients[pipeID]; } @@ -226,11 +208,7 @@ DumbPipe.registerOpener("socket", function(message, sender) { } socket.ondata = function(event) { - // Turn the buffer into a regular Array to traverse the mozbrowser boundary. - var array = Array.prototype.slice.call(new Uint8Array(event.data)); - array.constructor = Array; - - sender({ type: "data", data: array }); + sender({ type: "data", data: event.data }); } socket.ondrain = function(event) { @@ -264,3 +242,176 @@ DumbPipe.registerOpener("socket", function(message, sender) { } }; }); + +DumbPipe.registerOpener("audiorecorder", function(message, sender) { + var mediaRecorder = null; + var localAudioStream = null; + + function startRecording(localStream) { + localAudioStream = localStream; + + mediaRecorder = new MediaRecorder(localStream, { + mimeType: message.mimeType // 'audio/3gpp' // need to be certified app. + }); + + mediaRecorder.ondataavailable = function(e) { + if (e.data.size == 0) { + return; + } + + var fileReader = new FileReader(); + fileReader.onload = function() { + sender({ type: "data", data: fileReader.result }); + }; + fileReader.readAsArrayBuffer(e.data); + }; + + mediaRecorder.onstop = function(e) { + // Do nothing here, just relay the event. + // + // We can't close the pipe here, one reason is |onstop| is fired before |ondataavailable|, + // if close pipe here, there is no chance to deliever the recorded voice. Another reason is + // the recording might be stopped and started back and forth. So let's do the pipe + // closing on the other side instead, i.e. DirectRecord::nClose. + sender({ type: "stop" }); + }; + + mediaRecorder.onerror = function(e) { + sender({ type: "error" }); + }; + + mediaRecorder.onpause = function(e) { + sender({ type: "pause" }); + }; + + mediaRecorder.onstart = function(e) { + sender({ type: "start" }); + }; + + mediaRecorder.start(); + } + + return function(message) { + switch(message.type) { + case "start": + try { + if (!mediaRecorder) { + navigator.mozGetUserMedia({ + audio: true + }, function(localStream) { + startRecording(localStream); + }, function(e) { + sender({ type: "error", error: e }); + }); + } else if (mediaRecorder.state == "paused") { + mediaRecorder.resume(); + } else { + mediaRecorder.start(); + } + } catch (e) { + sender({ type: "error", error: e }); + } + break; + case "requestData": + try { + // An InvalidState error might be thrown. + mediaRecorder.requestData(); + } catch (e) { + sender({ type: "error", error: e }); + } + break; + case "pause": + try { + mediaRecorder.pause(); + } catch (e) { + sender({ type: "error", error: e }); + } + break; + case "stop": + try { + mediaRecorder.stop(); + localAudioStream.stop(); + mediaRecorder = null; + localAudioStream = null; + } catch (e) { + sender({ type: "error", error: e }); + } + break; + } + }; +}); + +DumbPipe.registerOpener("camera", function(message, sender) { + var mediaStream = null; + + var video = document.createElement("video"); + document.body.appendChild(video); + video.style.position = "absolute"; + video.style.visibility = "hidden"; + + video.addEventListener('canplay', function(ev) { + // We should use videoWidth and videoHeight, but they are unavailable (https://bugzilla.mozilla.org/show_bug.cgi?id=926753) + var getDimensions = setInterval(function() { + if (video.videoWidth > 0 && video.videoHeight > 0) { + clearInterval(getDimensions); + sender({ type: "gotstream", width: video.videoWidth, height: video.videoHeight }); + } + }, 50); + }, false); + + navigator.mozGetUserMedia({ + video: true, + audio: false, + }, function(localMediaStream) { + mediaStream = localMediaStream; + + video.src = URL.createObjectURL(localMediaStream); + video.play(); + }, function(err) { + console.log("Error: " + err); + }); + + return function(message) { + switch (message.type) { + case "setPosition": + video.style.left = message.x + "px"; + video.style.top = message.y + "px"; + video.style.width = message.w + "px"; + video.style.height = message.h + "px"; + break; + + case "setVisible": + video.style.visibility = message.visible ? "visible" : "hidden"; + break; + + case "snapshot": + var canvas = document.createElement("canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + var ctx = canvas.getContext("2d"); + ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight); + + canvas.toBlob(function(blob) { + var fileReader = new FileReader(); + + fileReader.onload = function(data) { + sender({ type: "snapshot", data: fileReader.result }); + } + + fileReader.readAsArrayBuffer(blob); + }, message.imageType); + break; + + case "close": + if (mediaStream) { + mediaStream.stop(); + } + + if (video.parentNode) { + document.body.removeChild(video); + } + + break; + } + }; +}); diff --git a/interpreter.ts b/interpreter.ts index 61e029574..7576ed2e3 100644 --- a/interpreter.ts +++ b/interpreter.ts @@ -95,6 +95,7 @@ module J2ME { if (frame.bci >= exception_table[i].start_pc && frame.bci <= exception_table[i].end_pc) { if (exception_table[i].catch_type === 0) { handler_pc = exception_table[i].handler_pc; + break; } else { var classInfo = resolve(exception_table[i].catch_type); if (isAssignableTo(ex.klass, classInfo.klass)) { diff --git a/java/Makefile b/java/Makefile index fe07ab700..8ec4691ad 100644 --- a/java/Makefile +++ b/java/Makefile @@ -1,5 +1,4 @@ -SRCS=$(shell find ./cldc1.1.1 -name *.java) $(shell find ./vm -name *.java) $(shell find ./midp -name *.java) -CUSTOM_SRCS=$(shell find ./custom -name *.java) +SRCS=$(shell find ./cldc1.1.1 -name *.java) $(shell find ./vm -name *.java) $(shell find ./midp -name *.java) $(shell find ./jsr-075 -name *.java) $(shell find ./custom -name *.java) JPP_DEFS=-DENABLE_JSR_205 -DENABLE_CHAMELEON -DENABLE_SSL -DENABLE_PUBLICKEYSTORE -DENABLE_JSR_211 -DENABLE_MULTIPLE_ISOLATES -DRECORD -DUSE_FILE_CONNECTION -DENABLE_JSR_234 JPP_SRCS=$(shell find . -name *.jpp) JPP_DESTS=$(JPP_SRCS:.jpp=.java) @@ -7,14 +6,15 @@ EXTRA=$(shell find . -name *.png) $(shell find . -name *.bin) $(shell find . -na VPATH=./cldc1.1.1 ./vm ./midp ./custom -classes.jar: $(SRCS) $(CUSTOM_SRCS) $(JPP_DESTS) - rm -rf build - mkdir build - javac -cp cldc1.1.1:vm:midp -source 1.3 -target 1.3 -bootclasspath "" -extdirs "" -d ./build $(SRCS) > /dev/null - javac -sourcepath custom -cp build -source 1.3 -target 1.3 -bootclasspath "" -extdirs "" -d ./build $(CUSTOM_SRCS) > /dev/null +classes.jar: $(SRCS) $(JPP_DESTS) + rm -rf build build-src + mkdir build build-src + cp -a cldc1.1.1/. vm/. midp/. jsr-075/. build-src/ + cp -a custom/. build-src/ + javac -cp build-src -source 1.3 -target 1.3 -bootclasspath "" -extdirs "" -d ./build `find ./build-src -name *.java` > /dev/null cd build && jar cvf0 ../classes.jar * jar uvf0 classes.jar $(EXTRA) - rm -rf build + rm -rf build build-src tools/Jpp.class: tools/Jpp.java javac $^ diff --git a/java/custom/com/nokia/mid/s40/codec/DataDecoder.java b/java/custom/com/nokia/mid/s40/codec/DataDecoder.java index 59152866b..d6b8a708a 100644 --- a/java/custom/com/nokia/mid/s40/codec/DataDecoder.java +++ b/java/custom/com/nokia/mid/s40/codec/DataDecoder.java @@ -30,6 +30,7 @@ public double getFloat(int tag) throws IOException { public native boolean getBoolean() throws IOException; public byte[] getByteArray() throws IOException { - throw new RuntimeException("DataDecoder::getByteArray() not implemented"); + System.out.println("DataDecoder::getByteArray() not implemented"); + return new byte[0]; } } diff --git a/java/custom/com/nokia/mid/ui/CustomKeyboardControl.java b/java/custom/com/nokia/mid/ui/CustomKeyboardControl.java index fdcc05aaf..42d5f644e 100644 --- a/java/custom/com/nokia/mid/ui/CustomKeyboardControl.java +++ b/java/custom/com/nokia/mid/ui/CustomKeyboardControl.java @@ -6,22 +6,37 @@ public interface CustomKeyboardControl { public static final int KEYBOARD_PORTRAIT = 2; public static final int KEYBOARD_PORTRAIT_180 = 8; - public void launch(int var1, int var2); - - public void launch(int var1); - + // `mode` can be one of the following: + // `VirtualKeyboard.VKB_MODE_DEFAULT`, + // `VirtualKeyboard.VKB_MODE_NUMERIC`, + // `VirtualKeyboard.VKB_MODE_ALPHA_LOWER_CASE`, + // `VirtualKeyboard.VKB_MODE_ALPHA_UPPER_CASE`, + // or `VirtualKeyboard.VKB_MODE_ALPHA_UPPER_CASE_LOCKED` + // + // `type` can be one of the following: + // `VirtualKeyboard.VKB_TYPE_ITUT`, + // or `VirtualKeyboard.VKB_TYPE_GAME` + public void launch(int type, int mode); + public void launch(int type); public void launch(); public void dismiss(); - public void setKeyboardType(int var1); + public void setKeyboardType(int type); - public void setKeyboardMode(int var1); + public void setKeyboardMode(int mode); public int getKeyboardMode(); public int getKeyboardType(); - public int getSupportedOrientations(int var1); + /** + * getSupportedOrientations + * + * @param vkbType keyboard type for which supported orientation is queried. + * must be `VirtualKeyboard.VKB_TYPE_ITUT` or `VirtualKeyboard.VKB_TYPE_GAME` + * @return supported orientations + */ + public int getSupportedOrientations(int vkbType); } diff --git a/java/custom/com/nokia/mid/ui/DeviceControl.java b/java/custom/com/nokia/mid/ui/DeviceControl.java index 03ceaf13b..ce2bd15ac 100644 --- a/java/custom/com/nokia/mid/ui/DeviceControl.java +++ b/java/custom/com/nokia/mid/ui/DeviceControl.java @@ -14,13 +14,9 @@ public static void flashLights(long duration) { throw new RuntimeException("DeviceControl::flashLights(long) not implemented"); } - public static void startVibra(int freq, long duration) { - System.out.println("DeviceControl::startVibra(int,long) not implemented"); - } + public static native void startVibra(int freq, long duration); - public static void stopVibra() { - System.out.println("DeviceControl::stopVibra() not implemented"); - } + public static native void stopVibra(); public static int getUserInactivityTime() { throw new RuntimeException("DeviceControl::getUserInactivityTime() not implemented"); diff --git a/java/custom/com/nokia/mid/ui/KeyboardVisibilityListener.java b/java/custom/com/nokia/mid/ui/KeyboardVisibilityListener.java index 9d3bf181b..a6a5fc9b0 100644 --- a/java/custom/com/nokia/mid/ui/KeyboardVisibilityListener.java +++ b/java/custom/com/nokia/mid/ui/KeyboardVisibilityListener.java @@ -1,8 +1,9 @@ package com.nokia.mid.ui; public interface KeyboardVisibilityListener { - public void showNotify(int var1); - - public void hideNotify(int var1); + // `keyboardCategory` will always be either + // `VirtualKeyboard.CUSTOM_KEYBOARD` or `VirtualKeyboard.SYSTEM_KEYBOARD` + public void showNotify(int keyboardCategory); + public void hideNotify(int keyboardCategory); } diff --git a/java/custom/com/nokia/mid/ui/VirtualKeyboard.java b/java/custom/com/nokia/mid/ui/VirtualKeyboard.java index 227e92b12..40696098a 100644 --- a/java/custom/com/nokia/mid/ui/VirtualKeyboard.java +++ b/java/custom/com/nokia/mid/ui/VirtualKeyboard.java @@ -3,6 +3,46 @@ import com.nokia.mid.ui.CustomKeyboardControl; import com.nokia.mid.ui.KeyboardVisibilityListener; +class VKVisibilityNotificationRunnable implements Runnable { + private Object listenerLock = new Object(); + + public void run() { + while(true) { + boolean isShow = sleepUntilVKVisibilityChange(); + synchronized(listenerLock) { + if (null == listener) { + continue; + } + // NB: A malicious or poorly-written listener can block + // in one of `showNotify` or `hideNotify`, causing + // us to stop sending visibility notifications and + // causing the next call to `setListener` to also + // block. We could alleviate this by creating a + // new Thread on which we will make the call to + // `showNotify` or `hideNotify`, then waiting for + // the thread to notify us but timing out after + // some reasonable amount of time. This is a lot + // of thread creation and will only help poorly- + // written programs so is probably overkill. + if (isShow) { + listener.showNotify(VirtualKeyboard.SYSTEM_KEYBOARD); + } else { + listener.hideNotify(VirtualKeyboard.SYSTEM_KEYBOARD); + } + } + } + } + + public void setListener(KeyboardVisibilityListener listener) { + synchronized(listenerLock) { + this.listener = listener; + } + } + + private KeyboardVisibilityListener listener; + private native boolean sleepUntilVKVisibilityChange(); +} + public class VirtualKeyboard { public static final int CUSTOM_KEYBOARD = 1; public static final int SYSTEM_KEYBOARD = 2; @@ -22,32 +62,30 @@ public static void hideOpenKeypadCommand(boolean bl) { System.out.println("VirtualKeyboard::hideOpenKeypadCommand(boolean) not implemented"); } - public static boolean isVisible() { - throw new RuntimeException("VirtualKeyboard::isVisible() not implemented"); - } + public native static boolean isVisible(); - public static int getXPosition() { - throw new RuntimeException("VirtualKeyboard::getXPosition() not implemented"); - } + public native static int getXPosition(); - public static int getYPosition() { - throw new RuntimeException("VirtualKeyboard::getYPosition() not implemented"); - } + public native static int getYPosition(); - public static int getWidth() { - throw new RuntimeException("VirtualKeyboard::getWidth() not implemented"); - } + public native static int getWidth(); - public static int getHeight() { - throw new RuntimeException("VirtualKeyboard::getHeight() not implemented"); - } + public native static int getHeight(); - public static void setVisibilityListener(KeyboardVisibilityListener keyboardVisibilityListener) { - System.out.println("VirtualKeyboard::setVisibilityListener(KeyboardVisibilityListener) not implemented"); + public static void setVisibilityListener(KeyboardVisibilityListener listener) { + if (null == visibilityNotifier) { + visibilityNotifier = new VKVisibilityNotificationRunnable(); + listenerThread = new Thread(visibilityNotifier); + listenerThread.start(); + } + visibilityNotifier.setListener(listener); } public static void suppressSizeChanged(boolean bl) { throw new RuntimeException("VirtualKeyboard::suppressSizeChanged(boolean) not implemented"); } + + private static VKVisibilityNotificationRunnable visibilityNotifier = null; + private static Thread listenerThread = null; } diff --git a/java/custom/java/io/DataOutputStream.java b/java/custom/java/io/DataOutputStream.java new file mode 100644 index 000000000..5e59fd1c9 --- /dev/null +++ b/java/custom/java/io/DataOutputStream.java @@ -0,0 +1,283 @@ +/* + * + * + * Copyright 1990-2007 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package java.io; + +/** + * A data output stream lets an application write primitive Java data + * types to an output stream in a portable way. An application can + * then use a data input stream to read the data back in. + * + * @version 12/17/01 (CLDC 1.1) + * @see java.io.DataInputStream + * @since JDK1.0, CLDC 1.0 + */ +public +class DataOutputStream extends OutputStream implements DataOutput { + + /** + * The output stream. + */ + protected OutputStream out; + + /** + * Creates a new data output stream to write data to the specified + * underlying output stream. + * + * @param out the underlying output stream, to be saved for later + * use. + */ + public DataOutputStream(OutputStream out) { + this.out = out; + } + + /** + * Writes the specified byte (the low eight bits of the argument + * b) to the underlying output stream. + *

+ * Implements the write method of OutputStream. + * + * @param b the byte to be written. + * @exception IOException if an I/O error occurs. + */ + public void write(int b) throws IOException { + out.write(b); + } + + /** + * Writes len bytes from the specified byte array + * starting at offset off to the underlying output stream. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + * @exception IOException if an I/O error occurs. + */ + public void write(byte b[], int off, int len) throws IOException { + out.write(b, off, len); + } + + /** + * Flushes this data output stream. This forces any buffered output + * bytes to be written out to the stream. + *

+ * The flush method of DataOutputStream + * calls the flush method of its underlying output stream. + * + * @exception IOException if an I/O error occurs. + */ + public void flush() throws IOException { + out.flush(); + } + + /** + * Closes this output stream and releases any system resources + * associated with the stream. + *

+ * The close method + * calls its flush method, and then calls the + * close method of its underlying output stream. + * + * @exception IOException if an I/O error occurs. + */ + public void close() throws IOException { + try { + flush(); + } catch (IOException e) { + } + out.close(); + } + + /** + * Writes a boolean to the underlying output stream as + * a 1-byte value. The value true is written out as the + * value (byte)1; the value false is + * written out as the value (byte)0. + * + * @param v a boolean value to be written. + * @exception IOException if an I/O error occurs. + */ + public final void writeBoolean(boolean v) throws IOException { + write(v ? 1 : 0); + } + + /** + * Writes out a byte to the underlying output stream as + * a 1-byte value. + * + * @param v a byte value to be written. + * @exception IOException if an I/O error occurs. + */ + public final void writeByte(int v) throws IOException { + write(v); + } + + /** + * Writes a short to the underlying output stream as two + * bytes, high byte first. + * + * @param v a short to be written. + * @exception IOException if an I/O error occurs. + */ + public final void writeShort(int v) throws IOException { + write((v >>> 8) & 0xFF); + write((v >>> 0) & 0xFF); + } + + /** + * Writes a char to the underlying output stream as a + * 2-byte value, high byte first. + * + * @param v a char value to be written. + * @exception IOException if an I/O error occurs. + */ + public final void writeChar(int v) throws IOException { + write((v >>> 8) & 0xFF); + write((v >>> 0) & 0xFF); + } + + /** + * Writes an int to the underlying output stream as four + * bytes, high byte first. + * + * @param v an int to be written. + * @exception IOException if an I/O error occurs. + */ + public final void writeInt(int v) throws IOException { + write((v >>> 24) & 0xFF); + write((v >>> 16) & 0xFF); + write((v >>> 8) & 0xFF); + write((v >>> 0) & 0xFF); + } + + /** + * Writes a long to the underlying output stream as eight + * bytes, high byte first. + * + * @param v a long to be written. + * @exception IOException if an I/O error occurs. + */ + public final void writeLong(long v) throws IOException { + write((int)(v >>> 56) & 0xFF); + write((int)(v >>> 48) & 0xFF); + write((int)(v >>> 40) & 0xFF); + write((int)(v >>> 32) & 0xFF); + write((int)(v >>> 24) & 0xFF); + write((int)(v >>> 16) & 0xFF); + write((int)(v >>> 8) & 0xFF); + write((int)(v >>> 0) & 0xFF); + } + + /** + * Converts the float argument to an int using the + * floatToIntBits method in class Float, + * and then writes that int value to the underlying + * output stream as a 4-byte quantity, high byte first. + * + * @param v a float value to be written. + * @exception IOException if an I/O error occurs. + * @see java.lang.Float#floatToIntBits(float) + * @since CLDC 1.1 + */ + public final void writeFloat(float v) throws IOException { + writeInt(Float.floatToIntBits(v)); + } + + /** + * Converts the double argument to a long using the + * doubleToLongBits method in class Double, + * and then writes that long value to the underlying + * output stream as an 8-byte quantity, high byte first. + * + * @param v a double value to be written. + * @exception IOException if an I/O error occurs. + * @see java.lang.Double#doubleToLongBits(double) + * @since CLDC 1.1 + */ + public final void writeDouble(double v) throws IOException { + writeLong(Double.doubleToLongBits(v)); + } + + /** + * Writes a string to the underlying output stream as a sequence of + * characters. Each character is written to the data output stream as + * if by the writeChar method. + * + * @param s a String value to be written. + * @exception IOException if an I/O error occurs. + * @see java.io.DataOutputStream#writeChar(int) + */ + public final void writeChars(String s) throws IOException { + int len = s.length(); + for (int i = 0 ; i < len ; i++) { + int v = s.charAt(i); + write((v >>> 8) & 0xFF); + write((v >>> 0) & 0xFF); + } + } + + /** + * Writes a string to the underlying output stream using UTF-8 + * encoding in a machine-independent manner. + *

+ * First, two bytes are written to the output stream as if by the + * writeShort method giving the number of bytes to + * follow. This value is the number of bytes actually written out, + * not the length of the string. Following the length, each character + * of the string is output, in sequence, using the UTF-8 encoding + * for the character. + * + * @param str a string to be written. + * @exception IOException if an I/O error occurs. + */ + public final void writeUTF(String str) throws IOException { + writeUTF(str, this); + } + + /** + * Writes a string to the specified DataOutput using UTF-8 encoding in a + * machine-independent manner. + *

+ * First, two bytes are written to out as if by the writeShort + * method giving the number of bytes to follow. This value is the number of + * bytes actually written out, not the length of the string. Following the + * length, each character of the string is output, in sequence, using the + * UTF-8 encoding for the character. + * + * @param str a string to be written. + * @param out destination to write to + * @return The number of bytes written out. + * @exception IOException if an I/O error occurs. + */ + static final int writeUTF(String str, DataOutput out) throws IOException { + byte[] bytearr = UTFToBytes(str); + out.write(bytearr); + return bytearr.length; + } + + static native byte[] UTFToBytes(String str); + +} diff --git a/java/custom/javax/bluetooth/LocalDevice.java b/java/custom/javax/bluetooth/LocalDevice.java deleted file mode 100644 index 60a957c46..000000000 --- a/java/custom/javax/bluetooth/LocalDevice.java +++ /dev/null @@ -1,8 +0,0 @@ -package javax.bluetooth; - -public class LocalDevice { - public static String getProperty(String property) { - System.out.println("LocalDevice::getProperty(String) not implemented."); - return null; - } -} diff --git a/java/custom/javax/microedition/io/file/FileSystemListener.java b/java/custom/javax/microedition/io/file/FileSystemListener.java deleted file mode 100644 index 4bcb09f43..000000000 --- a/java/custom/javax/microedition/io/file/FileSystemListener.java +++ /dev/null @@ -1,4 +0,0 @@ -package javax.microedition.io.file; - -public interface FileSystemListener { -} diff --git a/java/custom/javax/microedition/io/file/FileSystemRegistry.java b/java/custom/javax/microedition/io/file/FileSystemRegistry.java deleted file mode 100644 index 4710b69cd..000000000 --- a/java/custom/javax/microedition/io/file/FileSystemRegistry.java +++ /dev/null @@ -1,26 +0,0 @@ -package javax.microedition.io.file; - -import java.util.Enumeration; -import java.util.Vector; - -public class FileSystemRegistry { - private FileSystemRegistry() { - } - - public static boolean addFileSystemListener(FileSystemListener listener) { - System.out.println("FileSystemRegistry::addFileSystemListener(FileSystemListener) not implemented."); - return false; - } - - public static boolean removeFileSystemListener(FileSystemListener listener) { - System.out.println("FileSystemRegistry::removeFileSystemListener(FileSystemListener) not implemented."); - return false; - } - - public static Enumeration listRoots() { - Vector roots = new Vector(); - roots.addElement(""); - return roots.elements(); - } -} - diff --git a/java/jsr-075/com/ibm/oti/connection/file/Connection.java b/java/jsr-075/com/ibm/oti/connection/file/Connection.java new file mode 100644 index 000000000..f5ded4bb5 --- /dev/null +++ b/java/jsr-075/com/ibm/oti/connection/file/Connection.java @@ -0,0 +1,864 @@ +package com.ibm.oti.connection.file; + +/* + * Licensed Materials - Property of IBM, + * (c) Copyright IBM Corp. 2003 All Rights Reserved + */ + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Enumeration; +import java.util.NoSuchElementException; + +import javax.microedition.io.Connector; +import javax.microedition.io.file.ConnectionClosedException; +import javax.microedition.io.file.FileConnection; +import javax.microedition.io.file.IllegalModeException; + +import com.ibm.oti.connection.CreateConnection; + + +public class Connection implements FileConnection, CreateConnection { + + /* The url string that was sent to Connector.open(), + * without the preceeding "://" and host name. + * file separator is "/". + */ + private String fullPath; + + /* String representation of the path to this file + * on the file system. Includes the host name if there is any. + * file separator is the platform file separator. "/", "\\", etc. + */ + private String platformPath; + + /* Byte array representation of platformPath. + */ + private byte[] properPath; + + private String host; + private String root; + + /* The directory containing this file or dir. + * Might be null, if the connection is opened on a file system root. + */ + private String parentPath; + + /* The name of the file or directory, without the path info. + */ + private String name; + + /** boolean value indicating if the connection is open. + */ + private boolean open = false; + + /** The access mode the connection was opened with. + * possible values are : READ, WRITE or READ_WRITE + */ + private int accessMode; + + /** System dependant file separator character. + */ + public static final char separatorChar; + + /** System dependant file separator String. The initial value + * of this field is the System property "file.separator". + */ + public static final String separator; + + private FCInputStream inputStream = null; + private FCOutputStream outputStream = null; + + static { + com.ibm.oti.connection.file.Util.init(); + separator = Util.getSeparator(); + separatorChar= separator.charAt(0); + } + +/** + * Passes the parameters from the Connector.open() method to this + * object. Protocol used by MIDP 2.0 + * + * @author IBM + * @version initial + * + * @param spec String + * The address passed to Connector.open() + * @param access int + * The type of access this Connection is + * granted (READ, WRITE, READ_WRITE) + * @param timeout boolean + * A boolean indicating wether or not the + * caller to Connector.open() wants timeout + * exceptions or not + * @exception IOException + * If an error occured opening and configuring + * serial port. + * + * @see javax.microedition.io.Connector + */ +public javax.microedition.io.Connection setParameters2(String spec, int access, boolean timeout) throws IOException { + setParameters(spec, access, timeout); + return this; +} + + +/** + * Passes the parameters from the Connector.open() method to this + * object. + * + * @author IBM + * @version initial + * + * @param spec String + * The address passed to Connector.open() + * @param access int + * The type of access this Connection is + * granted (READ, WRITE, READ_WRITE) + * @param timeout boolean + * A boolean indicating whether or not the + * caller to Connector.open() wants timeout + * exceptions + * @exception IOException + * If an error occured opening and configuring + * serial port. + * + * @see javax.microedition.io.Connector + */ +public void setParameters(String spec, int accessMode, boolean timeout) throws IOException { + spec = decode(spec); + fullPath = validateSpec(spec); + setPaths(fullPath); + + if (name!="") + if (!isValidFilenameImpl(name.getBytes())) + throw new IllegalArgumentException("Invalid file name in FileConnection Url: " + spec); + + this.accessMode = accessMode; + checkSecurity(platformPath); + open = true; +} + +private void setPaths(String path) { + this.fullPath = path; + this.platformPath = getPlatformPath(fullPath); + this.properPath = platformPath.getBytes(); + parseDirectory(); +} + +private String getPlatformPath(String path) { + String platformPath =path.replace('/', separatorChar); + if (!host.equals("")) + platformPath = "\\\\" + host + separatorChar + platformPath; + return platformPath; +} + +private void checkSecurity(String path) { + SecurityManager security = System.getSecurityManager(); + if (security != null) { + if (accessMode == Connector.READ_WRITE) { + security.checkWrite(path); + security.checkRead(path); + } else if (accessMode == Connector.READ) + security.checkRead(path); + else + security.checkWrite(path); + } +} + +private void parseDirectory () { + String temp = fullPath; + + if(temp.substring(temp.length()-1,temp.length() ).equals("/")) { + temp = temp.substring(0, temp.length()-1); + } + + int idxforRoot = temp.indexOf("/"); + if (idxforRoot==-1) idxforRoot = temp.length(); + root= temp.substring(0,idxforRoot); + + if (root.equals("")) root = separator; + + if (idxforRoot==temp.length()) { + name = ""; + if (root.equals(separator)) + parentPath = "/" ; + else + parentPath = root + "/"; + } else { + int idx=-1; + while (temp.indexOf("/",idx+1)!=-1) { + idx = temp.indexOf("/",idx +1); + } + name = temp.substring(idx+1, temp.length()); + parentPath = temp.substring(0,idx+1); + } +} + + +/** + * Returns an indication of whether the file connection is currently + * open or not. + * + * @return true if the file connection is open, false otherwise. + */ +public boolean isOpen() { + return open; +} + + +public void close() throws IOException { + open = false; + inputStream = null; + outputStream = null; +} + +private void closeIOStreams() throws IOException { + if (outputStream != null) + outputStream.close(); + + if (inputStream != null) + inputStream.close(); +} + +public InputStream openInputStream() throws IOException { + if (!isOpen()) throw new IOException("Connection is closed"); + if (inputStream != null) throw new IOException("input stream already open"); + if (!existsInternal()) throw new IOException("File does not exist."); + if (isDirectoryInternal()) throw new IOException("Can not open InputStream on a directory."); + + checkRead(); + + inputStream = new FCInputStream(properPath, this); + return inputStream; +} + + public DataInputStream openDataInputStream() throws IOException { + return new DataInputStream(openInputStream()); + } + + public OutputStream openOutputStream() throws IOException { + if (outputStream != null) throw new IOException("output stream already open"); + if (!isOpen()) throw new IOException("Connection is closed"); + if (!existsInternal()) throw new IOException("File does not exist."); + if (isDirectoryInternal()) throw new IOException("Can not perform this operation on a directory."); + + checkWrite(); + + outputStream = new FCOutputStream(properPath, this); + return outputStream; + } + + public DataOutputStream openDataOutputStream() throws IOException { + return new DataOutputStream(openOutputStream()); + } + + public OutputStream openOutputStream(long offset) throws IOException{ + if (outputStream != null) throw new IOException("output stream already open"); + if (!isOpen()) throw new IOException("Connection is closed"); + if (!existsInternal()) throw new IOException("File does not exist."); + if (isDirectoryInternal()) throw new IOException("Can not perform this operation on a directory."); + + checkWrite(); + + if (offset<0) + throw new java.lang.IllegalArgumentException("offset can not be a negative value: " + offset); + + long fileSize = fileSizeImpl(properPath); + if (offset>fileSize) + offset= fileSize; + + outputStream = new FCOutputStream(properPath, offset, this); + return outputStream; + } + + /** + * This method is called from FCInputStream.close() to notify the connection that it is closed, + * and that a new InputStream could be opened again. + */ + protected synchronized void notifyInputStreamClosed() { + inputStream = null; + } + + /** + * This method is called from FCOutputStream.close() to notify the connection that it is closed, + * and that a new OutputStream could be opened again. + */ + protected synchronized void notifyOutputStreamClosed() { + outputStream = null; + } + + public long totalSize() { + if (!isOpen()) throw new ConnectionClosedException(); + // we need to check read access to the root, not the path + checkRead(root); + + return totalSizeImpl(root.getBytes()); + } + + private native long totalSizeImpl(byte[] root); + + public long availableSize() { + if (!isOpen()) throw new ConnectionClosedException(); + // we need to check read access to the root, not the path + checkRead(root); + + return availableSizeImpl(root.getBytes()); + } + + + + private native long availableSizeImpl(byte[] root); + + public long usedSize() { + if (!isOpen()) throw new ConnectionClosedException(); + // we need to check read access to the root, not the path + checkRead(root); + + return usedSizeImpl(root.getBytes()); + } + + private native long usedSizeImpl(byte[] root); + + + public long directorySize(boolean includeSubDirs) throws IOException { + if (!isOpen()) throw new ConnectionClosedException(); + checkRead(); + + if (!existsInternal()) return -1; + if (!isDirectoryInternal()) throw new IOException("Can not perform this operation on a file: " + getURL()); + + return directorySizeImpl(properPath, includeSubDirs); + } + + /* + * This method returns -1 if directory does not exist. + */ + private native long directorySizeImpl(byte[] path, boolean includeSubDirs); + + public long fileSize() throws IOException { + if (!isOpen()) throw new ConnectionClosedException(); + checkRead(); + if (isDirectoryInternal()) throw new IOException("Can not perform this operation on a directory: " + getURL()); + + return fileSizeImpl(properPath); + } + + /* + * This method returns -1 if file does not exist. + */ + private native long fileSizeImpl(byte[] path); + + + public boolean canRead() { + if (!isOpen()) throw new ConnectionClosedException(); + checkRead(); + return existsInternal() && !isWriteOnlyImpl(properPath); + } + + public boolean canWrite() { + if (!isOpen()) throw new ConnectionClosedException(); + checkRead(); + return existsInternal() && !isReadOnlyImpl(properPath); + } + + public boolean isHidden() { + if (!isOpen()) throw new ConnectionClosedException(); + checkRead(); + return existsInternal() && isHiddenImpl(properPath); + } + + public boolean isDirectory() { + if (!isOpen()) throw new ConnectionClosedException(); + checkRead(); + return isDirectoryInternal(); + } + + private boolean isDirectoryInternal() { + return isDirectoryImpl(properPath); + } + + private boolean isDirectoryInternal1() { + if (existsInternal()) { + return isDirectoryInternal(); + } else { + return fullPath.endsWith("/"); + } + } + + private native boolean isDirectoryImpl(byte[] path); + + public void setReadable(boolean readable) throws IOException { + if (!isOpen()) throw new ConnectionClosedException(); + checkWrite(); + if (!existsInternal()) throw new IOException("File does not exist: " + getURL()); + + setWriteOnlyImpl(properPath, !readable); + } + + public void setWritable(boolean writable) throws IOException { + if (!isOpen()) throw new ConnectionClosedException(); + checkWrite(); + if (!existsInternal()) throw new IOException("File does not exist: " + getURL()); + + setReadOnlyImpl(properPath, !writable); + } + + public void setHidden(boolean hidden) throws IOException { + if (!isOpen()) throw new ConnectionClosedException(); + checkWrite(); + if (!existsInternal()) throw new IOException("File does not exist: " + getURL()); + + setHiddenImpl(properPath, hidden); + } + + public Enumeration list() throws IOException { + return listInternal(null, false); + } + + public Enumeration list(String filter, boolean includeHidden) throws IOException { + if (filter==null) throw new NullPointerException(); + + //accept escaped filter string + filter = decode(filter); + + // replace the filter characters with a 'a' and see if it is a vaild filename + String filterWithoutWildcards = filter.replace('*', 'a'); + if (!isValidFilenameImpl(filterWithoutWildcards.getBytes())) + throw new IllegalArgumentException("filter contains an invalid character or path specification: " + filter); + + return listInternal(filter.getBytes(), includeHidden); + } + + private Enumeration listInternal(byte[] filter, boolean includeHidden) throws IOException { + // make the necessary checks + if (!isOpen()) throw new ConnectionClosedException(); + checkRead(); + if (!existsInternal()) throw new IOException("Directory does not exist: " + getURL()); + if (!isDirectoryInternal()) throw new IOException("Connection is open on a file: " + getURL()); + + // find the list of files and directories matching the filter + byte[][] implList = listImpl(properPath, filter, includeHidden ); + + // create an enumaration that will contain the list of files and directories in String form + int resultCount = implList==null ? 0 : implList.length; + final String result[] = new String[resultCount]; + + for (int index = 0; index < resultCount; index++) + result[index] = new String(implList[index], 0, implList[index].length); + + return new Enumeration() { + int pos = 0; + public boolean hasMoreElements() {return pos < result.length;} + public Object nextElement() { + if (pos < result.length) return result[pos++]; + throw new NoSuchElementException(); + } + }; + } + + /* + * @param path + * @param filter The filter to use when looking for files. It can contain valid filename characters + * and the wildcard character ('*'). If null is passed, then the default filter is + * used ("*") + * @param includeHidden + */ + private synchronized static native byte[][] listImpl(byte[] path, byte[] filter, boolean includeHidden); + + public void create() throws IOException { + if (!isOpen()) throw new ConnectionClosedException(); + checkWrite(); + + if (fullPath.endsWith("/")) throw new IOException("Connection is open on a directory: " + getURL()); + + int result = newFileImpl(properPath); + switch (result) { + case 0 : + return; + case 1 : + throw new IOException("Connection is open on an existing file: " + getURL()); + case 3 : + throw new IOException("Connection is open on a directory: "+ getURL()); + default: + throw new IOException("Con not create: "+ getURL()); + } + } + + private native int newFileImpl(byte[] path); + + public void mkdir() throws IOException { + if (!isOpen()) throw new ConnectionClosedException(); + checkWrite(); + + int result = mkdirImpl(properPath); + switch (result) { + case 0 : + return; + case 1 : + throw new IOException("Connection is open on an existing directory. " + getURL()); + case 3 : + throw new IOException("Connection is open on a file. " + getURL() ); + default : + throw new IOException("Can not mkdir: " + getURL()); + } + } + + private native int mkdirImpl(byte[] path); + + public boolean exists() { + if (!isOpen()) throw new ConnectionClosedException(); + checkRead(); + return existsInternal(); + } + + private boolean existsInternal() { + return existsImpl(properPath); + } + + private native boolean existsImpl(byte[] path); + + public void delete() throws IOException { + if (!isOpen()) throw new ConnectionClosedException(); + checkWrite(); + + if (isDirectoryInternal()) { + if (!deleteDirImpl(properPath)) + throw new IOException("Can not delete: " + getURL()); + } else { + closeIOStreams(); + if (!deleteFileImpl(properPath)) + throw new IOException("Can not delete: " + getURL()); + } + } + + private native boolean deleteDirImpl(byte[] path); + + private native boolean deleteFileImpl(byte[] path); + + public void rename(String newName) throws IOException { + if (!isOpen()) throw new ConnectionClosedException(); + checkWrite(); + if (!existsInternal()) throw new IOException("File does not exist: " + getURL()); + + if (newName==null) throw new NullPointerException(); + + //decode the file name before validation checks + newName = decode(newName); + + String newPath; + if (name.equals("")) { + newPath = newName; + } else { + // check for invalid characters + int idx = newName.indexOf('/'); + if (idx>-1 && (idx= fileSize()) + return; + if (offset < 0) + throw new IllegalArgumentException("Truncate offset can not be a negative value: " + offset); + + truncateImpl(properPath, offset); + } + + private native void truncateImpl(byte[] b, long offset); + + /** + * Checks the read access for this file connection's path + */ + private void checkRead() { + checkRead(platformPath); + } + + /** + * Checks the read access for a given path + */ + private void checkRead(String path) { + SecurityManager security = System.getSecurityManager(); + if (security != null) + security.checkRead(path); + if (accessMode == Connector.WRITE) + throw new IllegalModeException("Not open for read"); + } + + /** + * Checks the write access for this file connection's path + */ + private void checkWrite() { + SecurityManager security = System.getSecurityManager(); + if (security != null) + security.checkWrite(platformPath); + if (accessMode == Connector.READ) + throw new IllegalModeException("Not open for write"); + } + + public void setFileConnection(String fileName) throws IOException { + if (!isOpen()) throw new ConnectionClosedException(); + if (!isDirectoryInternal()) throw new IOException("Connection is not open on a directory: " + getURL()); + if (fileName==null) throw new NullPointerException(); + if (fileName.equals(".")) return; + + //decode the file name before validation checks + fileName = decode(fileName); + + // check for invalid characters + int idx = fileName.indexOf('/'); + if (idx>-1 && idx + * For example: '#' -> %23 + * + * @author IBM + * @version initial + * + * @return java.lang.String the string to be converted + * @param s java.lang.String the converted string + */ +public static String encode(String s) { + final String digits = "0123456789ABCDEF"; + StringBuffer buf = new StringBuffer(s.length()); + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || "-_.!~*\'()//:".indexOf(ch) > -1) + buf.append(ch); + else { + byte[] bytes = new String(new char[]{ch}).getBytes(); + for (int j=0; j> 4)); + buf.append(digits.charAt(bytes[j] & 0xf)); + } + } + } + return buf.toString(); +} + +/** + *

+ * '%' and two following hex digit characters are converted + * to the equivalent byte value. + * All other characters are passed through unmodified. + *

+ * e.g. "ABC %24%25" -> "ABC $%" + * + * @author IBM + * @version initial + * + * @param s java.lang.String + * The encoded string. + * @return java.lang.String + * The decoded version. + */ +public static String decode(String s) { + StringBuffer result = new StringBuffer(s.length()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + for (int i = 0; i < s.length();) { + char c = s.charAt(i); + if (c == '%') { + out.reset(); + do { + if (i + 2 >= s.length()) + throw new IllegalArgumentException("Incomplete % sequence at: " + i); + int d1 = Character.digit(s.charAt(i+1), 16); + int d2 = Character.digit(s.charAt(i+2), 16); + if (d1 == -1 || d2 == -1) + throw new IllegalArgumentException("Invalid % sequence (" + s.substring(i, i+3)+ ") at: " + String.valueOf(i)); + out.write((byte)((d1 << 4) + d2)); + i += 3; + } while (i < s.length() && s.charAt(i) == '%'); + result.append(out.toString()); + continue; + } else result.append(c); + i++; + } + return result.toString(); +} + +private native boolean isAbsoluteImpl(byte[] path); + +private native boolean isHiddenImpl(byte[] path); + +private native boolean isReadOnlyImpl(byte[] path); + +private native boolean isWriteOnlyImpl(byte[] path); + +private native void setReadOnlyImpl(byte[] path, boolean value); + +private native void setWriteOnlyImpl(byte[] path, boolean value); + +private native void setHiddenImpl(byte[] path, boolean value); + +private native boolean isValidFilenameImpl(byte[] filename); + +/* + * Sets fullPath and host + * + */ +private String validateSpec(String spec) { + String fullPath; + + if (!spec.startsWith("//")) + throw new IllegalArgumentException("File connection url should start with 'file://': " + spec); + + spec = spec.substring(2); + + if (separatorChar!='/'){ + if (spec.indexOf(separatorChar) != -1) + throw new IllegalArgumentException("File Connection url can not include the file separator: " + spec); + } + + int idx = spec.indexOf("/"); + if (idx==-1) + throw new IllegalArgumentException(spec); + + if (idx==0) { + // no host is specified, since the URL started as "file:///" + host=""; + fullPath=spec.substring(1,spec.length()) ; + } else { + // There is a host specified + // make sure it is a valid specification. + + int idx2 = spec.indexOf(":"); + int idx3 = spec.indexOf(" "); + if ((idx2!=-1 && idx2 + * Device internal filesystems in memory may also be accessed through + * this class as well, provided there is underlying hardware and OS support. + * If file connections are not supported to a particular media or file system, + * attempts to open a file connection to the media or file system through + * Connector.open() results in an + * javax.microedition.io.IOException being thrown. + *

+ *

Establishing a Connection

+ *

The format of the input string used to access a FileConnection through + * Connector.open() must follow the format of a fully-qualified, + * absolute path file name as described by the file + * URL format in IETF RFCs 1738 & 2396. Further detail for the File + * URL format can be found in the javax.microedition.io.file package description. + *

+ *

A single connection object only references a single file or + * directory at a time. In some cases, a connection object can be reused to + * refer to a different file or directory than it was originally associated with + * (see {@link #setFileConnection}). In general, the best approach to reference + * a different file or directory is by establishing a completely separate + * connection through the Connector.open() method.

+ * + *

FileConnection Behavior

+ * + *

File connection is different from other Generic Connection Framework + * connections in that a connection object can be successfully returned from + * the Connector.open() method without actually referencing an + * existing entity (in this case, a file or directory). This behavior + * allows the creation of new files and directories on a file system. + * For example, the following code can be used to create + * a new file on a file system, where CFCard is a valid existing file + * system root name for a given implementation:

+ * + *
+ * try {
+ *     FileConnection fconn = (FileConnection)Connector.open("file:///CFCard/newfile.txt");
+ *     // If no exception is thrown, then the URI is valid, but the file may or may not exist.
+ *     if (!fconn.exists())
+ *         fconn.create();  // create the file if it doesn't exist
+ *
+ *     fconn.close();
+ * }
+ * catch (IOException ioe) {
+ * }
+ *
+ *

+ * Developers should always check for the file's or directory's existence + * after a connection is established to determine if the file or directory + * actually exists. Similarly, files or directories can be deleted using the + * {@link #delete} method, and developers should close the connection + * immediately after deletion to prevent exceptions from accessing a connection + * to a non-existent file or directory. + *

+ *

+ * A file connection's open status is unaffected by the opening and closing of + * input and output streams from the file connection; the file connection stays + * open until close() is invoked on the FileConnection instance. + * Input and output streams may be opened and closed multiple times on a + * FileConnection instance. + *

+ *

+ * All FileConnection instances have one underlying + * InputStream and one OutputStream. Opening a + * DataInputStream counts as opening an InputStream, + * and opening a DataOutputStream counts as opening an + * OutputStream. A FileConnection instance can have + * only one InputStream and one OutputStream open at + * any one time. Trying to open more than one InputStream or more + * than one OutputStream from a StreamConnection + * causes an IOException. Trying to open an InputStream + * or an OutputStream after the FileConnection has + * been closed causes an IOException. + *

+ *

+ * The inherited StreamConnection methods in a + * FileConnection instance are not synchronized. The only stream + * method that can be called safely from another thread is close. + * When close is invoked on a stream that is executing in another + * thread, any pending I/O method MUST throw an + * InterruptedIOException. In the above case, implementations + * SHOULD try to throw the exception in a timely manner. When all open streams + * have been closed, and when the FileConnection is closed, any + * pending I/O operations MUST be interrupted in a timely manner. + *

+ *

+ * Data written to the output streams of these FileConnection + * objects is not guaranteed to be flushed to the stream's destination + * (and subsequently made available to any input streams) until either + * flush() or close() is invoked on the stream. + *

+ *

Security

+ *

+ * Access to file connections is restricted to prevent unauthorized manipulation + * of data. The access security model applied to the file connection + * is defined by the implementing profile. The security model is applied on + * the invocation of the Connector.open() method with a valid file + * connection string. The mode provided in the open() method + * (Connector.READ_WRITE by default) indicates the application's + * request for access rights for the indicated file or directory and is + * therefore checked against the security scheme. All three connections modes + * (READ_WRITE, WRITE_ONLY, and READ_ONLY) are + * supported for a file connection and determine the access requested from the + * security model. + *

+ *

+ * The security model is also applied during use of the returned + * FileConnection, specifically when the methods + * openInputStream(), openDataInputStream(), + * openOutputStream(), and openDataOutputStream() are + * invoked. These methods have implied request for access rights (i.e. + * input stream access is requesting read access, and output stream access is + * requesting write access). Should the application not be granted the + * appropriate read or write access to the file or file system by the profile + * authorization scheme, a java.lang.SecurityException is thrown. + *

+ *

+ * File access through the File Connection API may be restricted to files that + * are within a public context and not deemed private or sensitive. This + * restriction is intended to protect the device's and other users' files and + * data from both malicious and unintentional access. RMS databases cannot be + * accessed using the File Connection API. Access to files and directories that + * are private to another application, files and directories that are private to + * a different user than the current user, system configuration files, and + * device and OS specific files and directories may be restricted. In these + * situations, a java.lang.SecurityException is thrown from the + * Connector.open() method if the file, file system, or directory + * is not allowed to be accessed. + *

+ * + * @see FileSystemRegistry + * @since FileConnection 1.0 + */ + +public interface FileConnection extends javax.microedition.io.StreamConnection { + + /** + * Returns an indication of whether the file connection is currently + * open or not. + * + * @return true if the file connection is open, false otherwise. + */ + public boolean isOpen(); + + /** + * Open and return an input stream for a connection. The connection's + * target must already exist and be accessible for the input stream to be created. + * + * @return An open input stream + * @throws IOException if an I/O error occurs, if the method is invoked on + * a directory, if the connection's target does not + * yet exist, or the connection's target is not accessible. + * @throws IllegalModeException if the application does have read access + * to the connection's target but has opened the connection in + * Connector.WRITE mode. + * @throws SecurityException If the application is not granted read + * access to the connection's target. + */ + public InputStream openInputStream() throws IOException; + + /** + * Open and return a data input stream for a connection. The connection's + * target must already exist and be accessible for the input stream to be created. + * + * @return An open input stream + * @throws IOException If an I/O error occurs, if the method is invoked on + * a directory, if the connection's target does not + * yet exist, or the connection's target is not accessible. + * @throws IllegalModeException if the application does have read access + * to the connection's target but has opened the connection in + * Connector.WRITE mode. + * @throws SecurityException If the application is not granted read + * access to the connection's target. + */ + public DataInputStream openDataInputStream() throws IOException; + + /** + * Open and return an output stream for a connection. The output stream + * is positioned at the start of the file. Writing data to the output stream + * overwrites the contents of the files (i.e. does not insert data). + * Writing data to output streams beyond the current end of file + * automatically extends the file size. The connection's target must + * already exist and be accessible for the output stream to be created. + * {@link #openOutputStream(long)} should be used to position an output + * stream to a different position in the file. + *

+ * Changes made to a file through an output stream may not be immediately + * made to the actual file residing on the file system because + * platform and implementation specific use of caching and buffering of the + * data. Steam contents and file length extensions are not necessarily + * visible outside of the application immediately unless + * flush() is called on the stream. The returned output stream + * is automatically and synchronously flushed when it is closed. + *

+ * + * @return An open output stream + * @throws IOException If an I/O error occurs, if the method is invoked on + * a directory, the file does not yet exist, or the connection's + * target is not accessible. + * @throws IllegalModeException if the application does have write access + * to the connection's target but has opened the connection in + * Connector.READ mode. + * @throws SecurityException If the application is not granted write + * access to the connection's target. + * @see #openOutputStream(long) + */ + public OutputStream openOutputStream() throws IOException; + + /** + * Open and return a data output stream for a connection. The output stream + * is positioned at the start of the file. Writing data to the output stream + * overwrites the contents of the files (i.e. does not insert data). + * Writing data to output streams beyond the current end of file + * automatically extends the file size. The connection's target must + * already exist and be accessible for the output stream to be created. + * {@link #openOutputStream(long)} should be used to position an output + * stream to a different position in the file. + *

+ * Changes made to a file through an output stream may not be immediately + * made to the actual file residing on the file system because + * platform and implementation specific use of caching and buffering of the + * data. Steam contents and file length extensions are not necessarily + * visible outside of the application immediately unless + * flush() is called on the stream. The returned output stream + * is automatically and synchronously flushed when it is closed. + *

+ * + * @return An open output stream + * @throws IOException If an I/O error occurs, if the method is invoked on + * a directory, the file does not yet exist, or the connection's + * target is not accessible. + * @throws IllegalModeException if the application does have write access + * to the connection's target but has opened the connection in + * Connector.READ mode. + * @throws SecurityException If the application is not granted write + * access to the connection's target. + * @see #openOutputStream(long) + */ + public DataOutputStream openDataOutputStream() throws IOException; + + /** + * This method opens an output stream and positions it at the indicated + * byte offset in the file. Data written to the returned output stream at + * that position overwrites any existing data until EOF is reached, and + * then additional data is appended. The connection's target must + * already exist and be accessible for the output stream to be created. + *

+ * Changes made to a file through an output stream may not be immediately + * made to the actual file residing on the file system because + * platform and implementation specific use of caching and buffering of the + * data. Steam contents and file length extensions are not necessarily + * visible outside of the application immediately unless + * flush() is called on the stream. The returned output + * stream is automatically and synchronously flushed when it is closed. + *

+ * + * @param byteOffset number of bytes to skip over from the beginning of + * the file when positioning the start of the OutputStream. If + * the provided offset is larger than or equal to the current file + * size, the OutputStream is positioned at the current end of the + * file for appending. + * @return an open OutputStream positioned at the byte offset in the file, + * or the end of the file if the offset is greater than the size + * of the file. + * @throws IOException If an I/O error occurs, if the method is invoked + * on a directory, the file does not yet exist, or the + * connection's target is not accessible. + * @throws IllegalModeException if the application does have write access + * to the connection's target but has opened the connection in + * Connector.READ mode. + * @throws SecurityException if the security if the application does not + * allow write access to the file. + * @throws IllegalArgumentException if byteOffset has a negative value. + */ + public OutputStream openOutputStream(long byteOffset) throws IOException; + + /** + * Determines the total size of the file system the connection's target + * resides on. + * + * @return The total size of the file system in bytes, or -1 if the + * file system is not accessible. + * @throws SecurityException if the security of the application does not + * have read access to the root volume. + * @throws IllegalModeException if the application does have read access + * to the connection's target but has opened the connection in + * Connector.WRITE mode. + * @throws ConnectionClosedException if the connection is closed. + */ + public long totalSize(); + + + /** + * Determines the free memory that is available on the file system the file + * or directory resides on. This may only be an estimate and may vary based + * on platform-specific file system blocking and metadata information. + * + * @return The available size in bytes on a file system, or -1 if the + * file system is not accessible. + * @throws SecurityException if the security of the application does not + * have read access to the root volume. + * @throws IllegalModeException if the application does have read access + * to the directory but has opened the connection in + * Connector.WRITE mode. + * @throws ConnectionClosedException if the connection is closed. + */ + public long availableSize(); + + + /** + * Determines the used memory of a file system the connection's target + * resides on. This may only be an estimate and may vary based + * on platform-specific file system blocking and metadata information. + * + * @return The used size of bytes on a file system, or -1 if the file + * system is not accessible. + * @throws SecurityException if the security of the application does not + * have read access to the root volume. + * @throws IllegalModeException if the application does have read access + * to the directory but has opened the connection in + * Connector.WRITE mode. + * @throws ConnectionClosedException if the connection is closed. + */ + public long usedSize(); + + + /** + * Determines the size in bytes on a file system of all of the files + * that are contained in a directory. + * + * @param includeSubdirs If set to true, the method determines + * the size of the given directory and all subdirs + * recursively. If false, the method returns the size + * of the files in the directory only. + * @return The size in bytes occupied by the files included in the + * directory, or -1 if the directory does not exist or is not + * accessible. + * @throws IOException if the method is invoked on a file. + * @throws SecurityException if the security of the application does not + * have read access for the directory. + * @throws IllegalModeException if the application does have read access + * to the directory but has opened the connection in + * Connector.WRITE mode. + * @throws ConnectionClosedException if the connection is closed. + */ + public long directorySize(boolean includeSubDirs) throws IOException; + + + /** + * Determines the size of a file on the file system. The size of a file + * always represents the number of bytes contained in the file; there is + * no pre-allocated but empty space in a file. + *

+ * fileSize() always returns size of the file on the file system, + * and not in any pending output stream. flush() should be used + * before calling fileSize() to ensure the contents + * of the output streams opened to the file get written to the file system. + *

+ * + * @return The size in bytes of the selected file, or -1 if the + * file does not exist or is not accessible. + * @throws IOException if the method is invoked on a directory. + * @throws SecurityException if the security of the application does not + * have read access for the file. + * @throws IllegalModeException if the application does have read access + * to the file but has opened the connection in + * Connector.WRITE mode. + * @throws ConnectionClosedException if the connection is closed. + */ + public long fileSize() throws IOException; + + + /** + * Checks if the file or directory is readable. This method checks the + * attributes associated with a file or directory by the underlying file + * system. Some file systems may not support associating attributes with + * a file, in which case this method returns true. + * + * @return true if the connection's target exists, is accessible, and + * is readable, otherwise false. + * @throws SecurityException if the security of the application does not + * have read access for the connection's target. + * @throws IllegalModeException if the application does have read access + * to the connection's target but has opened the connection in + * Connector.WRITE mode. + * @throws ConnectionClosedException if the connection is closed. + * @see #setReadable + */ + public boolean canRead(); + + + /** + * Checks if the file or directory is writable. This method checks the + * attributes associated with a file or directory by the underlying file + * system. Some file systems may not support associating attributes with + * a file, in which case this method returns true. + * + * @return true if the connection's target exists, is accessible, and + * is writable, otherwise false. + * @throws SecurityException if the security of the application does not + * have read access for the connection's target. + * @throws IllegalModeException if the application does have read access + * to the connection's target but has opened the connection in + * Connector.WRITE mode. + * @throws ConnectionClosedException if the connection is closed. + * @see #setWritable + */ + public boolean canWrite(); + + + /** + * Checks if the file is hidden. The exact definition of hidden is + * system-dependent. For example, on UNIX systems a file is considered to be + * hidden if its name begins with a period character ('.'). On Win32 and + * FAT file systems, a file is considered to be hidden if it has been marked + * as such in the file's attributes. If hidden files are not supported on + * the referenced file system, this method always returns false. + * + * @return true if the file exists, is accessible, and is hidden, + * otherwise false. + * @throws ConnectionClosedException if the connection is closed. + * @throws SecurityException if the security of the application does not + * have read access for the connection's target. + * @throws IllegalModeException if the application does have read access + * to the connection's target but has opened the connection in + * Connector.WRITE mode. + * @see #setHidden + */ + public boolean isHidden(); + + + /** + * Sets the file or directory readable attribute to the + * indicated value. The readable attribute for the file on the actual + * file system is set immediately upon invocation of this method. If the + * file system doesn't support a settable read attribute, this method is + * ignored and canRead() always returns true. + * + * @param readable The new state of the readable flag of the selected file. + * @throws IOException of the connection's target does not exist or is not + * accessible. + * @throws ConnectionClosedException if the connection is closed. + * @throws SecurityException if the security of the application does not + * have write access to the connection's target. + * @throws IllegalModeException if the application does have write access + * to the connection's target but has opened the connection in + * Connector.READ mode. + * @see #canRead + */ + public void setReadable(boolean readable) throws IOException; + + + /** + * Sets the selected file or directory writable attribute to the + * indicated value. The writable attribute for the file on the actual + * file system is set immediately upon invocation of the method. If the + * file system doesn't support a settable write attribute, this method is + * ignored and canWrite() always returns true. + * + * @param writable The new state of the writable flag of the selected file. + * @throws IOException if the connection's target does not exist or is not + * accessible. + * @throws ConnectionClosedException if the connection is closed. + * @throws SecurityException if the security of the application does not + * have write access to the connection's target. + * @throws IllegalModeException if the application does have write access + * to the connection's target but has opened the connection in + * Connector.READ mode. + * @see #canWrite + */ + public void setWritable(boolean writable)throws IOException; + + + /** + * Sets the hidden attribute of the selected file to + * the value provided. The attribute is applied to the file on the actual + * file system immediately upon invocation of this method if the file system + * and platform support it. If the file system doesn't support a hidden + * attribute, this method is ignored and isHidden() always + * returns false. Since the exact definition of hidden is system-dependent, + * this method only works on file systems that support a settable file + * attribute. For example, on Win32 and FAT file systems, a file may be + * considered hidden if it has been marked as such in the file's + * attributes; therefore this method is applicable. However on UNIX + * systems a file may be considered to be hidden if its name begins with a + * period character ('.'). In the UNIX case, this method may be ignored and + * the method to make a file hidden may be the rename() method. + * + * @param hidden The new state of the hidden flag of the selected file. + * @throws IOException if the connection's target does not exist or is not + * accessible. + * @throws ConnectionClosedException if the connection is closed. + * @throws SecurityException if the security of the application does not + * have write access to the connection's target. + * @throws IllegalModeException if the application does have write access + * to the connection's target but has opened the connection in + * Connector.READ mode. + * @see #isHidden + */ + public void setHidden(boolean hidden) throws IOException; + + + /** + * Gets a list of all files and directories contained in a directory. + * The directory is the connection's target as specified in + * Connector.open(). + * + * @return An Enumeration of strings, denoting the files and + * directories in the directory. The string returned contain only + * the file or directory name and does not contain any path prefix + * (to get a complete path for each file or directory, prepend + * {@link #getPath}). Directories are denoted with a + * trailing slash "/" in their returned name. The Enumeration has + * zero length if the directory is empty. Any + * hidden files and directories in the directory are not included + * in the returned list. Any current directory indication (".") + * and any parent directory indication ("..") is not included + * in the list of files and directories returned. + * @throws IOException if invoked on a file, the directory does not exist, + * the directory is not accessible, or an I/O error occurs. + * @throws ConnectionClosedException if the connection is closed. + * @throws SecurityException if the security of the application does not + * have read access for the directory. + * @throws IllegalModeException if the application does have read access + * to the directory but has opened the connection in + * Connector.WRITE mode. + */ + public Enumeration list() throws IOException; + + /** + * Gets a filtered list of files and directories contained in a directory. + * The directory is the connection's target as specified in + * Connector.open(). + * + * @param filter String against which all files and directories are + * matched for retrieval. An asterisk ("*") can be used as a + * wildcard to represent 0 or more occurrences of any character. + * @param includeHidden boolean indicating whether files marked as hidden + * should be included or not in the list of files and directories + * returned. + * @return An Enumeration of strings, denoting the files and directories + * in the directory matching the filter. Directories are denoted + * with a trailing slash "/" in their returned name. The + * Enumeration has zero length if the directory is empty or no + * files and/or directories are found matching the given filter. + * Any current directory indication (".") and any parent directory + * indication ("..") is not included in the list of files and + * directories returned. + * @throws IOException if invoked on a file, the directory does not exist, + * the directory is not accessible, or an I/O error occurs. + * @throws ConnectionClosedException if the connection is closed. + * @throws SecurityException if the security of the application does not + * have read access for the directory. + * @throws IllegalModeException if the application does have read access + * to the connection's target but has opened the connection in + * Connector.WRITE mode. + * @throws NullPointerException if filter is + * null. + * @throws IllegalArgumentException if filter contains any path + * specification or is an invalid filename for the platform + * (e.g. contains characters invalid for a filename on the platform). + */ + public Enumeration list(String filter, boolean includeHidden) throws IOException; + + /** + * Creates a file corresponding to the file string + * provided in the Connector.open() method for this FileConnection. The + * file is created immediately on the actual file system upon invocation of + * this method. Files are created with zero length and data can be put into + * the file through output streams opened on the file. This method does not + * create any directories specified in the file's path. + * + * @throws SecurityException if the security of the application does not + * have write access for the file. + * @throws IllegalModeException if the application does have write access + * to the file but has opened the connection in + * Connector.READ mode. + * @throws IOException if invoked on an existing file or on any directory + * (mkdir() is used to create directories), the + * connection's target has a trailing "/" to denote a + * directory, the target file system is not accessible, or an + * unspecified error occurs preventing creation of the file. + * @throws ConnectionClosedException if the connection is closed. + */ + public void create() throws IOException; + + + /** + * Creates a directory corresponding to the directory + * string provided in the Connector.open() method. + * The directory is created immediately on the actual + * file system upon invocation of this method. Directories in the + * specified path are not recursively created and must be explicitly + * created before subdirectories can be created. + * + * @throws SecurityException if the security of the application does not + * have write access to the directory. + * @throws IllegalModeException if the application does have write access + * to the directory but has opened the connection in + * Connector.READ mode. + * @throws IOException if invoked on an existing directory or on any file + * (create() is used to create files), the target + * file sytem is not accessible, or an unspecified error occurs + * preventing creation of the directory. + * @throws ConnectionClosedException if the connection is closed. + */ + public void mkdir() throws IOException; + + + /** + * Checks if the file or directory specified in the URL passed to the + * Connector.open() method exists. + * + * @return true if the connnection's target exists and is accessible, + * otherwise false. + * @throws SecurityException if the security of the application does not + * have read access for the connection's target. + * @throws IllegalModeException if the application does have read access + * to the connection's target but has opened the connection in + * Connector.WRITE mode. + * @throws ConnectionClosedException if the connection is closed. + */ + public abstract boolean exists(); + + /** + * Checks if the URL passed to the Connector.open() is a directory. + * + * @return True if the connection's target exists, is accessible, and + * is a directory, otherwise false. + * @throws SecurityException if the security of the application does not + * have read access for the connection's target. + * @throws ConnectionClosedException if the connection is closed. + * @throws IllegalModeException if the application does have read access + * to the connection's target but has opened the connection in + * Connector.WRITE mode. + */ + public boolean isDirectory(); + + /** + * Deletes the file or directory specified in the + * Connector.open() URL. The file or directory is deleted immediately on + * the actual file system upon invocation of this method. All open input + * and output streams are automatically flushed and closed. Attempts to + * further use those streams result in an IOException. The + * FileConnection instance object remains open and available for use. + * + * @throws ConnectionClosedException if the connection is closed. + * @throws SecurityException if the security of the application does not + * have write access to the connection's target. + * @throws IllegalModeException if the application does have write access + * to the connection's target but has opened the connection in + * Connector.READ mode. + * @throws IOException If the target is a directory and it is not empty, + * the connection target does not exist or is unaccessible, or + * an unspecified error occurs preventing deletion of the target. + */ + public void delete() throws java.io.IOException; + + /** + * Renames the selected file or directory to a new name in the same + * directory. The file or directory is renamed immediately on the actual + * file system upon invocation of this method. No file or directory by the + * original name exists after this method call. All previously open + * input and output streams are automatically flushed and closed. Attempts + * to further use those streams result in an IOException. The + * FileConnection instance object remains open and available for use, + * referring now to the file or directory by its new name. + * + * @param newName The new name of the file or directory. The name must + * not contain any path specification; the file or directory + * remains in its same directory as before this method call. + * @throws IOException if the connection's target does not exist, the + * connection's target is not accessible, a file or directory + * already exists by the newName, or + * newName is an invalid filename for the platform + * (e.g. contains characters invalid in a filename on the platform). + * @throws ConnectionClosedException if the connection is closed. + * @throws SecurityException if the security of the application does not + * have write access to the connection's target. + * @throws IllegalModeException if the application does have write access + * to the connection's target but has opened the connection in + * Connector.READ mode. + * @throws NullPointerException if newName is + * null. + * @throws IllegalArgumentException if newName contains any + * path specification. + */ + public abstract void rename(String newName) throws IOException; + + /** + * Truncates the file, discarding all data from the given byte offset to + * the current end of the file. If the byte offset provided is greater + * than or equal to the file's current byte count, the method returns + * without changing the file. Any open streams are flushed automatically + * before the truncation occurs. + * + * @param byteOffset the offset into the file from which truncation + * occurs. + * @throws IOException if invoked on a directory or the file does not exist + * or is not accessible. + * @throws ConnectionClosedException if the connection is closed. + * @throws SecurityException if the security of the application does not + * have write access to the file. + * @throws IllegalModeException if the application does have write access + * to the file but has opened the connection in + * Connector.READ mode. + * @throws IllegalArgumentException if byteOffset is + * less than zero. + */ + public abstract void truncate(long byteOffset) throws IOException; + + /** + * Resets this FileConnection object to another file or directory. This + * allows reuse of the FileConnection object for directory traversal. The + * current FileConnection object must refer to a directory, and the new + * file or directory must exist within this directory, or may be the + * string ".." used to indicate the parent directory for the current + * connection). The FileConnection instance object remains open and + * available for use, referring now to the newly specified file or + * directory. + * + * @param fileName name of the file or directory to which this + * FileConnection is reset. The fileName must be one of the + * values returned from the {@link #list} method, or the string + * ".." to indicate the parent directory of the current + * connection. The fileName must not contain any additional path + * specification; i.e. the file or directory must reside within + * the current directory. + * @throws NullPointerException if fileName is + * null. + * @throws SecurityException if the security of the application does not + * have the security access to the specified file or directory + * as requested in the Connector.open method + * invocation that originally opened this FileConnection. + * @throws IllegalArgumentException if fileName contains any + * path specification or does not yet exist. + * @throws IOException if the current FileConnection is opened on a file, + * the connection's target is not accessible, or + * fileName is an invalid filename for the platform + * (e.g. contains characters invalid in a filename on the platform). + * @throws ConnectionClosedException if the connection is closed. + */ + public abstract void setFileConnection(String fileName) throws IOException; + + /** + * Returns the name of a file or directory excluding the URL schema and + * all paths. Directories are denoted with a + * trailing slash "/" in their returned name. The String resulting + * from this method looks as follows: + *
+     *   <directory>/
+     * 
+ * or + *
+     *   <filename.extension>
+     * 
+ * or if no file extension + *
+     *   <filename>
+     * 
+ * + * @return The name of a file or directory. + */ + public String getName(); + + /** + * Returns the path excluding the file or directory name and the "file" URL + * schema and host from where the file or directory specified in the + * Connector.open() method is opened. {@link #getName} can be appended to + * this value to get a fully qualified path filename. The String resulting + * from this method looks as follows: + *
+     *   /<root>/<directory>/
+     * 
+ * + * @return The path of a file or directory in the format specified above. + */ + public String getPath(); + + + /** + * Returns the full file URL including + * the scheme, host, and path from where the file or directory specified + * in the Connector.open() method is opened. The string returned is in + * an escaped ASCII format as defined by RFC 2396. + * The resulting String looks as follows: + *
+     *   file://<host>/<root>/<directory>/<filename.extension>
+     * 
+ * or + *
+     *   file://<host>/<root>/<directory>/<directoryname>/
+     * 
+ * + * @return The URL of a file or directory in the format specified above. + */ + public String getURL(); + + + /** + * Returns the time that the file denoted by the URL specified + * in the Connector.open() method was last modified. + * + * @return A long value representing the time the file was last + * modified, measured in milliseconds since the epoch + * (00:00:00 GMT, January 1, 1970), or 0L if an I/O error occurs. + * If modification date is not supported by the underlying platform + * and/or file system, then 0L is also returned. + * If the connection's target does not exist or is not + * accessible, 0L is returned. + * @throws SecurityException if the security of the application does not + * have read access for the connection's target. + * @throws IllegalModeException if the application does have read access + * to the connection's target but has opened the connection in + * Connector.WRITE mode. + * @throws ConnectionClosedException if the connection is closed. + */ + public long lastModified(); +} diff --git a/java/jsr-075/javax/microedition/io/file/FileSystemListener.java b/java/jsr-075/javax/microedition/io/file/FileSystemListener.java new file mode 100755 index 000000000..9a760ba35 --- /dev/null +++ b/java/jsr-075/javax/microedition/io/file/FileSystemListener.java @@ -0,0 +1,45 @@ +package javax.microedition.io.file; + +/* + * Licensed Materials - Property of IBM, + * (c) Copyright IBM Corp. 2003 All Rights Reserved + */ + +/** + * This class is used for receiving status notification when + * adding or removing a file system root. This can be achieved by inserting or + * removing a card from a device or by mounting or unmounting file systems to + * a device. + * + * @see FileConnection + * @since FileConnection 1.0 + */ +public interface FileSystemListener { + + /** + * Constant indicating that a file system root has been added to the device. + */ + public static final int ROOT_ADDED = 0; + + /** + * Constant indicating that a file system root has been removed from the + * device. + */ + public static final int ROOT_REMOVED = 1; + + /** + * This method is invoked when a root on the device has changed state. + * + * @param state int representing the state change that has happened to + * the root. + * @param rootName the String name of the root, following the root naming + * conventions detailed in FileConnection. + * @throws IllegalArgumentException if state has a negative + * value or is not one of the legal acceptable constants. + * @throws NullPointerException if rootName is + * null. + * + * @see FileConnection + */ + public abstract void rootChanged(int state, String rootName); +} diff --git a/java/jsr-075/javax/microedition/io/file/FileSystemRegistry.java b/java/jsr-075/javax/microedition/io/file/FileSystemRegistry.java new file mode 100755 index 000000000..f87698276 --- /dev/null +++ b/java/jsr-075/javax/microedition/io/file/FileSystemRegistry.java @@ -0,0 +1,188 @@ +package javax.microedition.io.file; + +/* + * Licensed Materials - Property of IBM, + * (c) Copyright IBM Corp. 2003 All Rights Reserved + */ + +import java.util.Enumeration; +import java.util.NoSuchElementException; +import java.util.Vector; + +/** + * The FileSystemRegistry is a central registry for file system listeners + * interested in the adding and removing (or mounting and unmounting) of + * file systems on a device. + * + * @see FileConnection + * @see FileSystemListener + * @since FileConnection 1.0 + */ +public class FileSystemRegistry { + + // the registered listeners + private static Vector listeners = new Vector(); + + // whether the natives have been initialized + private static boolean isInitialized = false; + + // used to synchronize init operation + private static Object initLock = new Object(); + + static { + com.ibm.oti.connection.file.Util.init(); + } + + /* Prevent JavaDoc from generating default constructor */ + FileSystemRegistry() { + } + + /** + * This method is used to register a FileSystemListener that is + * notified in case of adding and removing a new file system root. + * Multiple file system listeners can be added. If file + * systems are not supported on a device, false is returned from + * the method (this check is performed prior to security checks). + * + * @param listener The new FileSystemListener to be registered in + * order to handle adding/removing file system roots. + * @return boolean indicating if file system listener was successfully + * added or not + * @throws SecurityException if application is not given permission + * to read files. + * @throws NullPointerException if listener is null. + * + * @see FileSystemListener + * @see FileConnection + */ + public static boolean addFileSystemListener(FileSystemListener listener) { + if (listener==null) + throw new NullPointerException(); + if (listeners.contains(listener)) + return false; + + listeners.addElement(listener); + + synchronized(initLock) { + if (!isInitialized) { + initImpl(); + isInitialized = true; + } + } + + return true; + } + + + /** + * This method is used to remove a registered FileSystemListener. If file + * systems are not supported on a device, false is returned from + * the method. + * + * @param listener The FileSystemListener to be removed. + * @return boolean indicating if file system listener was successfully + * removed or not + * @throws NullPointerException if listener is null. + * + * @see FileSystemListener + * @see FileConnection + */ + public static boolean removeFileSystemListener(FileSystemListener listener) { + if (listener==null) + throw new NullPointerException(); + + int idxElement = listeners.indexOf(listener); + if (idxElement==-1) + return false; + + listeners.removeElementAt(idxElement); + + return true; + } + + + /** + * This method returns the currently mounted root file systems on a device + * as String objects in an Enumeration. + * If there are no roots available on the device, a zero length Enumeration + * is returned. If file systems are not supported on a device, a zero + * length Enumeration is also returned (this check is performed prior to + * security checks). + *

+ * The first directory in the file URI is referred to as the root, + * which corresponds to a logical mount point for a particular storage + * unit or memory. Root strings are defined by the platform or + * implementation and can be a string of zero or more characters ("" can be + * a valid root string on some systems) followed by a trailing "/" to denote + * that the root is a directory. Each root string is guaranteed to uniquely + * refer to a root. Root names are + * device specific and are not required to adhere to any standard. + * Examples of possible root strings and how to open them include: + * + * + * + * + * + * + * + *
Possible Root ValueOpening the Root
CFCard/Connector.open("file:///CFCard/");
SDCard/Connector.open("file:///SDCard/");
MemoryStick/Connector.open("file:///MemoryStick/");
C:/Connector.open("file:///C:/");
/Connector.open("file:////");
+ *

+ * The following is a sample showing the use of listRoots to retrieve + * the available size of all roots on a device: + *

+	 *   Enumeration rootEnum = FileSystemRegistry.listRoots();
+	 *   while (e.hasMoreElements()) {
+	 *      String root = (String) e.nextElement();
+	 *      FileConnection fc = Connector.open("file:///" + root);
+	 *      System.out.println(fc.availableSize());
+	 *   } 
+	 * 
+ * + * @return an Eumeration of mounted file systems as String objects. + * @throws SecurityException if application is not given permission + * to read files. + * @see FileConnection + */ + public static Enumeration listRoots() { + final String[] roots = getRootsImpl(); + + return new Enumeration() { + int i = 0; + public boolean hasMoreElements() { + return roots!=null && iLocalDeviceImpl. + * BCC is supposed to communicate with native application which is a central + * authority for local Bluetooth device settings. + * + * To simplify porting efforts, all methods of this class work with Bluetooth + * addresses (presented as Java strings), instead of using RemoteDeviceImpl + * objects. Conversion between the two is performed elsewhere. + */ +public abstract class BCC { + + /* Keeps the only instance of this class. */ + private static BCC instance = null; + + /* + * Protects the constructor to prevent unauthorized instantiation. + */ + protected BCC() { + } + + /* + * Retrieves instance of this class. + * + * @return instance of this class + */ + public synchronized static BCC getInstance() { + if (instance == null) { + instance = new NativeBCC(); + } + return instance; + } + + /* + * Enables Bluetooth radio and the Bluetooth protocol stack for use. + * + * @return true if the operation succeeded, false otherwise + */ + public abstract boolean enableBluetooth(); + + /* + * Queries the power state of the Bluetooth device. + * + * @return true is the Bluetooth device is on, + * false otherwise. + */ + public abstract boolean isBluetoothEnabled(); + + /* + * Returns local Bluetooth address. + * + * @return local Bluetooth address. + */ + public abstract String getBluetoothAddress(); + + /* + * Returns user-friendly name for the local device. + * + * @return user-friendly name for the local device, or + * null if the name could not be retrieved + */ + public abstract String getFriendlyName(); + + /* + * Retrieves the user-friendly name for specified remote device. + * + * @param address Bluetooth address of a remote device + * @return name of the remote device, or + * null if the name could not be retrieved + */ + public abstract String getFriendlyName(String address); + + /* + * Determines if the local device is in connectable mode. + * + * @return true if the device is connectable, false otherwise + */ + public abstract boolean isConnectable(); + + // JAVADOC COMMENT ELIDED + public abstract DeviceClass getDeviceClass(); + + // JAVADOC COMMENT ELIDED + public abstract boolean setServiceClasses(int classes); + + // JAVADOC COMMENT ELIDED + public abstract int getAccessCode(); + + // JAVADOC COMMENT ELIDED + public abstract boolean setAccessCode(int accessCode); + + /* + * Checks if the local device has a bond with a remote device. + * + * @param address Bluetooth address of a remote device + * @return true if the two devices were paired, false otherwise + */ + public abstract boolean isPaired(String address); + + /* + * Checks if a remote device was authenticated. + * + * @param address Bluetooth address of a remote device + * @return true if the device was authenticated, false otherwise + */ + public abstract boolean isAuthenticated(String address); + + /* + * Checks if a remote device is trusted (authorized for all services). + * + * @param address Bluetooth address of a remote device + * @return true if the device is trusted, false otherwise + */ + public abstract boolean isTrusted(String address); + + /* + * Checks if connections to a remote device are encrypted. + * + * @param address Bluetooth address of the remote device + * @return true if connections to the device are encrypted, false otherwise + */ + public abstract boolean isEncrypted(String address); + + /* + * Retrieves PIN code to use for pairing with a remote device. If the + * PIN code is not known, PIN entry dialog is displayed. + * + * @param address the Bluetooth address of the remote device + * @return string containing the PIN code + */ + public abstract String getPasskey(String address); + + /* + * Initiates pairing with a remote device. + * + * @param address the Bluetooth address of the device with which to pair + * @param pin an array containing the PIN code + * @return true if the device was authenticated, false otherwise + */ + public abstract boolean bond(String address, String pin); + + /* + * Authenticates remote device. + * + * @param address Bluetooth address of a remote device + * @return true if the device was authenticated, false otherwise + */ + public abstract boolean authenticate(String address); + + /* + * Authorizes a Bluetooth connection. + * + * @param address the Bluetooth address of the remote device + * @param handle handle for the service record of the srvice the remote + * device is trying to access + * @return true if authorization succeeded, false otherwise + */ + public abstract boolean authorize(String address, int handle); + + /* + * Enables or disables encryption of data exchanges. + * + * @param address the Bluetooth address of the remote device + * @param enable specifies whether the encryption should be enabled + * @return true if the encryption has been changed, false otherwise + */ + public abstract boolean encrypt(String address, boolean enable); + + /* + * Returns the list of preknown devices. + * + * @return vector containing preknown devices; + * null if there is no preknown devices . + */ + public abstract Vector getPreknownDevices(); + + /* + * Checks if there is a connection to the remote device. + * + * @param address the Bluetooth address of the remote device + * @return true if connection is established with the remote device + */ + public abstract boolean isConnected(String address); + + /* + * Registers a new connection to a remote device. + * For the real mode makes nothing currently. + * + * @param address the Bluetooth address of the remote device + */ + public void addConnection(String address) {} + + /* + * Unregisters an existing connection to a remote device. + * For the real mode makes nothing currently. + * + * @param address the Bluetooth address of the remote device + */ + public void removeConnection(String address) {} + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/BluetoothConnection.java b/java/midp/com/sun/jsr082/bluetooth/BluetoothConnection.java new file mode 100644 index 000000000..35696a0b7 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/BluetoothConnection.java @@ -0,0 +1,286 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import java.io.IOException; +import com.sun.jsr082.obex.ObexPacketStream; +import com.sun.jsr082.obex.ObexTransport; + +import javax.bluetooth.RemoteDevice; +import javax.bluetooth.BluetoothConnectionException; +import javax.microedition.io.Connection; +import javax.microedition.io.Connector; + +/* + * Base class for all bluetooth connections. + */ +abstract public class BluetoothConnection { + + /* Keeps requested connection details. */ + protected BluetoothUrl url; + + /* Keeps open mode. */ + protected int mode; + + /* true if this connection was authorized, false otherwise. */ + private boolean authorized; + + /* true if this connection has requested encryption and is encrypted. */ + private boolean encrypted; + + /* Remote device for this connection. */ + private RemoteDevice remoteDevice; + + /* + * Retrieves BluetoothConnection from given one. + * Connection given is supposed to be either Bluetooth connection + * or a connection that uses Bluetooth as transport. All involved + * connections supposed to be open. + * + * @param conn the connection to extract Bluetooth connection from + * @return proper BluetoothConnection instance + * @throws IllegalArgumentException if connection is neither an instance + * of BluetoothConnection, nor uses one as transport. + * @throws IOException if connection given or transport is closed or + * transport is invalid. + */ + public static BluetoothConnection getConnection(Connection conn) + throws IOException { + if (conn == null) { + throw new NullPointerException("Null connection specified."); + } + + if (conn instanceof ObexPacketStream) { + conn = ((ObexPacketStream)conn).getTransport(); + } + + if (!(conn instanceof BluetoothConnection)) { + throw new IllegalArgumentException("The specified connection " + + "is not a Bluetooth connection."); + } + + BluetoothConnection btConn = (BluetoothConnection)conn; + btConn.checkOpen(); + return btConn; + } + + /* + * Creates a new instance of this class. + * + * @param url connection url + * @param mode I/O access mode server side otherwise it's false + */ + protected BluetoothConnection(BluetoothUrl url, int mode) { + // IMPL_NOTE: find proper place; the intent here is to start EmulationPolling + // and SDPServer prior to create a user's notifier + SDDB.getInstance(); + + this.url = url; + this.mode = mode; + } + + /* + * Returns remote device for this connection. + * + * @return RemoteDevice object for this connection + * @throws IOException if this connection is closed + */ + public RemoteDevice getRemoteDevice() throws IOException { + checkOpen(); + return remoteDevice; + } + + /* + * Returns Bluetooth address of the remote device for this connection. + * + * @return Bluetooth address of the remote device + */ + public abstract String getRemoteDeviceAddress(); + + /* + * Retrieves reference to the remote device for this connection. + */ + protected void setRemoteDevice() { + remoteDevice = DiscoveryAgentImpl.getInstance(). + getRemoteDevice(getRemoteDeviceAddress()); + BCC.getInstance().addConnection(getRemoteDeviceAddress()); + } + + /* + * Removes reference to the remote device. + */ + protected void resetRemoteDevice() { + if (encrypted) { + encrypt(false); + } + remoteDevice = null; + BCC.getInstance().removeConnection(getRemoteDeviceAddress()); + } + + /* + * Determines if this connection is closed. + * + * @return true if this connection is closed, false otherwise + */ + public boolean isClosed() { + return remoteDevice == null; + } + + /* + * Determines whether this connection represents the server side, + * i.e. this connection was created by a notifier in acceptAndOpen(). + * + * @return true if this connection is a server-side connection, + * false otherwise + */ + public boolean isServerSide() { + return url.isServer; + } + + /* + * Returns the authorization state of this connection. + * + * @return true if this connection has been authorized, false otherwise + */ + public boolean isAuthorized() { + return authorized; + } + + /* + * Authorizes this connection. It is assumed that the remote device has + * previously been authenticated. This connection must represent the server + * side, i.e. isServer() should return true. + * + * @return true if the operation succeeded, false otherwise + */ + public boolean authorize() { + authorized = BCC.getInstance().authorize( + remoteDevice.getBluetoothAddress(), getServiceRecordHandle()); + return authorized; + } + + /* + * Changes encryption for this connection. + * + * @param enable specifies whether encription should be turned on or off + * @return true if encryption has been set as required, false otherwise + */ + public boolean encrypt(boolean enable) { + if (enable == encrypted) { + return true; + } + BCC.getInstance().encrypt(remoteDevice.getBluetoothAddress(), enable); + if (remoteDevice.isEncrypted()) { + if (enable) { + encrypted = true; + return true; + } + encrypted = false; + return false; + } else { + if (enable) { + return false; + } + encrypted = false; + return true; + } + } + + /* + * Returns handle for the service record of the service this connection + * is attached to. Valid for server-side (incoming) connections only. + * + * @return service record handle, or 0 if the handle is not available + */ + protected int getServiceRecordHandle() { + return 0; + } + + /* + * Checks if this connection is open. + * + * @throws IOException if this connection is closed + */ + protected void checkOpen() throws IOException { + if (isClosed()) { + throw new IOException("Connection is closed."); + } + } + + /* + * Performs security checks, such as authentication, authorization, and + * encryption setup. + * + * @throws BluetoothConnectionException when failed + */ + protected void checkSecurity() + throws BluetoothConnectionException, IOException { + if (url.authenticate) { + if (!remoteDevice.authenticate()) { + throw new BluetoothConnectionException( + BluetoothConnectionException.SECURITY_BLOCK, + "Authentication failed."); + } + } + if (url.authorize) { + if (!remoteDevice.authorize((Connection)this)) { + throw new BluetoothConnectionException( + BluetoothConnectionException.SECURITY_BLOCK, + "Authorization failed."); + } + } + if (url.encrypt) { + if (!remoteDevice.encrypt((Connection)this, true)) { + throw new BluetoothConnectionException( + BluetoothConnectionException.SECURITY_BLOCK, + "Encryption failed."); + } + } + } + + /* + * Checks read access. + * + * @throws IOException if open mode does not permit read access + */ + protected void checkReadMode() throws IOException { + if ((mode & Connector.READ) == 0) { + throw new IOException("Invalid mode: " + mode); + } + } + + /* + * Checks write access. + * + * @throws IOException if open mode does not permit write access + */ + protected void checkWriteMode() throws IOException { + if ((mode & Connector.WRITE) == 0) { + throw new IOException("Invalid mode: " + mode); + } + } + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/BluetoothEvent.java b/java/midp/com/sun/jsr082/bluetooth/BluetoothEvent.java new file mode 100644 index 000000000..6f28a615a --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/BluetoothEvent.java @@ -0,0 +1,53 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; + +/* + * Represents Bluetooth stack event, such as inquiry completion, etc. + */ +abstract class BluetoothEvent { + + public static String eventName = null; + + /* + * Called immediately upon event retrieval. The default behavior is to + * enqueue this event into the Dispatcher's event storage. + */ + public void dispatch() { + Dispatcher.enqueue(this); + } + + /* + * Processes this event. + */ + abstract public void process(); + + + public String toString() { + return ("Event: " + eventName); + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/BluetoothNotifier.java b/java/midp/com/sun/jsr082/bluetooth/BluetoothNotifier.java new file mode 100644 index 000000000..d115cbcff --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/BluetoothNotifier.java @@ -0,0 +1,180 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import java.io.IOException; +import javax.microedition.io.Connection; +import javax.bluetooth.ServiceRecord; +import javax.bluetooth.ServiceRegistrationException; +import javax.bluetooth.DataElement; +import java.util.Enumeration; + +/* + * Base class for all bluetooth notifiers. + */ +public abstract class BluetoothNotifier implements Connection { + + /* Flag to identify if this notifier is closed. */ + protected boolean isClosed = false; + + /* Bluetooth url this notifier created with. */ + protected BluetoothUrl url; + + /* Service record that describes represented service. */ + protected ServiceRecordImpl serviceRec = null; + + /* Keeps open mode. */ + protected int mode; + + /* + * Class constructor. + * + * @param url server connection string this notifier was created with + * @param mode I/O access mode + */ + protected BluetoothNotifier(BluetoothUrl url, int mode) { + // IMPL_NOTE: find proper place; the intent here is to start EmulationPolling + // and SDPServer prior to create a user's notifier + SDDB.getInstance(); + this.url = url; + this.mode = mode; + } + + /* + * Retrieves service record for this notifier. + * It always returns the same object reference. + * + * @return service record associated with this notifier + * @throws IllegalArgumentException if the notifier is closed + */ + ServiceRecord getServiceRecord() { + if (isClosed) { + throw new IllegalArgumentException("Notifier is closed."); + } + // IMPL_NOTE: copy should probably be returned instead of a reference, + // but the current implementation returns reference to make TCK pass + // return serviceRec.copy(); + return serviceRec; + } + + /* + * Stores the service record for this notifier in the local SDDB. + * If there is no SDDB version of the service record, this method will + * do nothing. + * + * @param record new service record value + * @throws IllegalArgumentException if new record is invalid + * + * @throws ServiceRegistrationException if the record cannot be + * updated successfully in the SDDB + */ + protected void updateServiceRecord(ServiceRecordImpl record) + throws ServiceRegistrationException { + ServiceRecordImpl oldRecord = serviceRec; + serviceRec = record.copy(); + try { + checkServiceRecord(); + } catch (ServiceRegistrationException e) { + serviceRec = oldRecord; + throw new IllegalArgumentException(e.getMessage()); + } + if (SDDB.getInstance().contains(serviceRec)) { + SDDB.getInstance().updateServiceRecord(serviceRec); + } + } + + /* + * Ensures that the service record is valid. + * + * @throws ServiceRegistrationException in case described in the + * JSR specification + */ + protected abstract void checkServiceRecord() + throws ServiceRegistrationException; + + /* + * Compares two DataElements. + * + * @param first first DataElement + * @param second second DataElement + * @return true if elements are equal, false otherwise + * @see javax.bluetooth.DataElement + */ + protected boolean compareDataElements(DataElement first, + DataElement second) { + boolean ret = false; + int valueType = first.getDataType(); + if (ret = (valueType == second.getDataType())) { + switch (valueType) { + case DataElement.BOOL: + ret = first.getBoolean() == second.getBoolean(); + break; + case DataElement.U_INT_1: + case DataElement.U_INT_2: + case DataElement.U_INT_4: + case DataElement.INT_1: + case DataElement.INT_2: + case DataElement.INT_4: + case DataElement.INT_8: + ret = first.getLong() == second.getLong(); + break; + default: + Object v1 = first.getValue(); + Object v2 = second.getValue(); + if (v1 instanceof Enumeration && v2 instanceof Enumeration) { + Enumeration e1 = (Enumeration)v1; + Enumeration e2 = (Enumeration)v2; + ret = true; + while (e1.hasMoreElements() && + e2.hasMoreElements() && ret) { + ret &= e1.nextElement().equals(e2.nextElement()); + } + ret = ret && + !(e1.hasMoreElements() || + e2.hasMoreElements()); + } else if (v1 instanceof byte[] && v2 instanceof byte[]) { + byte[] a1 = (byte[])v1; + byte[] a2 = (byte[])v2; + ret = a1.length == a2.length; + for (int i = a1.length; --i >= 0 && ret; ) { + ret &= (a1[i] == a2[i]); + } + } else { + ret = v1.equals(v2); + } + break; + } + } + return ret; + } + + /* + * Closes the connection. Connection interface implementation. + * + * @throws IOException if an I/O error occurs + */ + public abstract void close() throws IOException; +} diff --git a/java/midp/com/sun/jsr082/bluetooth/BluetoothProtocol.java b/java/midp/com/sun/jsr082/bluetooth/BluetoothProtocol.java new file mode 100644 index 000000000..724199efa --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/BluetoothProtocol.java @@ -0,0 +1,195 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; + +import com.sun.j2me.io.ConnectionBaseInterface; +import com.sun.j2me.security.BluetoothPermission; +import java.io.IOException; +import java.io.InterruptedIOException; +import javax.microedition.io.Connection; +import javax.microedition.io.Connector; +import javax.bluetooth.L2CAPConnection; +import javax.bluetooth.BluetoothConnectionException; + +import com.sun.j2me.app.AppPackage; +import com.sun.j2me.main.Configuration; + +/* + * Provides abstract base for bluetooth protocols. + */ +public abstract class BluetoothProtocol implements ConnectionBaseInterface { + /* Particular protocol type. */ + private int protocol; + + /* Keeps set of fields specified by URL. */ + protected BluetoothUrl url = null; + + /* + * Constructs an instance. + * @param protocol specifies particular protocol, must be one of + * BluetoothUrl.L2CAP, BluetoothUrl.RFCOMM, BluetoothUrl.OBEX + */ + protected BluetoothProtocol(int protocol) { + this.protocol = protocol; + } + + /* + * Implements the openPrim() of + * ConnectionBaseInerface and allows to get + * connection by means of Connector.open() + * call. + * + * @param name the target for the connection + * @param mode I/O access mode + * @param timeouts ignored + * + * @return L2CAP connection open. + * @exception IOException if opening connection fails. + */ + public Connection openPrim(String name, int mode, boolean timeouts) + throws IOException { + return openPrimImpl(new BluetoothUrl(protocol, name), mode); + } + + + /* + * Checks permissions and opens requested connection. + * + * @param token security token passed by calling class + * @param url BluetoothUrl instance that defines required + * connection stringname the URL without protocol name and colon + * @param mode connector.READ_WRITE or connector.READ or connector.WRITE + * + * @return a notifier in case of server connection string, open connection + * in case of client one. + * + * @exception IOException if opening connection fails. + */ + protected Connection openPrimImpl(BluetoothUrl url, int mode) + throws IOException { + checkOpenMode(mode); + checkUrl(url); + this.url = url; + + return url.isServer? + serverConnection(mode): + clientConnection(mode); + } + + /* + * Ensures open mode requested is READ_WRITE or READ or WRITE + * + * @param mode open mode to be checked + * @exception IllegalArgumentException if mode given is invalid + * + * IMPL_NOTE check if other modes are needed + */ + private void checkOpenMode(int mode) throws IllegalArgumentException { + if (mode != Connector.READ_WRITE && + mode != Connector.READ && + mode != Connector.WRITE) { + throw new IllegalArgumentException("Unsupported mode: " + mode); + } + } + + /* + * Ensures URL parameters have valid values. This implementation contains + * common checks and is called from subclasses before making protocol + * specific ones. + * + * @param url URL to check + * @exception IllegalArgumentException if invalid url parameters found + * @exception BluetoothConnectionException if url parameters are not + * acceptable due to Bluetooth stack limitations + */ + protected void checkUrl(BluetoothUrl url) + throws IllegalArgumentException, BluetoothConnectionException { + + /* + * IMPL_NOTE: revisit this code if TCK changes. + * IllegalArgumentException seems to be right one here, not + * BluetoothConnectionException. However TCK expects the latter + * in several cases. Once IllegalArgumentException becomes + * preferable this check can be placed to BluetoothUrl. + * Questionable TCK tests: + * bluetooth.Connector.Security.openClientTests, + * bluetooth.Connector.Security.openServerTests + */ + if ((url.encrypt || url.authorize) && !url.authenticate) { + throw new BluetoothConnectionException( + BluetoothConnectionException.UNACCEPTABLE_PARAMS, + "Invalid Authenticate parameter"); + } + } + + /* + * Ensures that permissions are proper and creates client side connection. + * @param token security token if passed by caller, or null + * client side connection. + * @param mode I/O access mode + * @return connection created, defined in subclasses + * @exception IOException if opening connection fails. + */ + protected abstract Connection clientConnection(int mode) + throws IOException; + + /* + * Ensures that permissions are proper and creates required notifier at + * server side. + * @param token security token if passed by caller, or null + * @param mode I/O access mode + * @return server notifier, defined in subclasses + * @exception IOException if opening connection fails. + */ + protected abstract Connection serverConnection(int mode) + throws IOException; + + /* + * Makes sure caller has the com.sun.midp permission set to "allowed". + * + * @param token security token of the calling class, may be null + * @param permission requested permission ID + * + * @exception IOInterruptedException if another thread interrupts the + * calling thread while this method is waiting to preempt the + * display. + */ + protected void checkForPermission(BluetoothPermission permission) + throws InterruptedIOException { + + AppPackage app = AppPackage.getInstance(); + + try { + app.checkForPermission(new BluetoothPermission( + permission.getName(), url.getResourceName())); + } catch (InterruptedException ie) { + throw new InterruptedIOException( + "Interrupted while trying to ask the user permission"); + } + } +} + diff --git a/java/midp/com/sun/jsr082/bluetooth/BluetoothPush.java b/java/midp/com/sun/jsr082/bluetooth/BluetoothPush.java new file mode 100644 index 000000000..c6afc6540 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/BluetoothPush.java @@ -0,0 +1,210 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; + +import java.io.IOException; +import javax.bluetooth.DiscoveryAgent; +import com.sun.jsr082.bluetooth.btl2cap.L2CAPNotifierImpl; +import com.sun.jsr082.bluetooth.btspp.BTSPPNotifierImpl; + +/* + * Bluetooth connect-anytime services support class. + */ +public class BluetoothPush { + + /* 'authenticated' AllowedSender parameter. */ + private static final String AUTHENTICATED = ";AUTHENTICATED"; + + /* 'authorized' AllowedSender parameter. */ + private static final String AUTHORIZED = ";AUTHORIZED"; + + /* 'blacklist' AllowedSender parameter. */ + private static final String BLACKLIST = ";BLACKLIST="; + + /* Service record serializer instance. */ + private static ServiceRecordSerializer srs = new ServiceRecordSerializer(); + + /* + * Checks if the specified URL is valid. + * + * @param url URL to verify + * @throws IllegalArgumentException if the URL is malformed + */ + public static void verifyUrl(String url) { + BluetoothUrl btUrl = new BluetoothUrl(url); + if ((btUrl.encrypt || btUrl.authorize) && !btUrl.authenticate) { + throw new IllegalArgumentException( + "'authenticate=true' parameter is required."); + } + } + + /* + * Checks if the specified AllowedSender field is valid. + * + * @param filter filter to verify + * @throws IllegalArgumentException if the filter is malformed + */ + public static void verifyFilter(String filter) { + if (filter.length() == 0) { + throw new IllegalArgumentException(); + } + filter = filter.toUpperCase(); + int i = 0; + while (i < filter.length() && i < 12) { + char c = filter.charAt(i++); + if (c == '*' || c == '?') { + continue; + } + if (c == ';') { + i--; + if (i == 0) { + throw new IllegalArgumentException( + "Invalid Bluetooth address."); + } + break; + } + if ((c < '0' || c > '9') && (c < 'A' || c > 'F')) { + throw new IllegalArgumentException( + "Invalid Bluetooth address."); + } + } + filter = filter.substring(i); + if (filter.length() == 0) { + return; + } + if (filter.startsWith(AUTHENTICATED)) { + filter = filter.substring(AUTHENTICATED.length()); + } else if (filter.startsWith(AUTHORIZED)) { + filter = filter.substring(AUTHORIZED.length()); + } + if (filter.length() == 0) { + return; + } + if (!filter.startsWith(BLACKLIST)) { + throw new IllegalArgumentException("Invalid parameter."); + } + filter = filter.substring(BLACKLIST.length()); + if (filter.length() == 0) { + throw new IllegalArgumentException("Invalid blacklist."); + } + int count = 0; + while (true) { + if (++count > 1024) { + throw new IllegalArgumentException("Blacklist too long."); + } + i = 0; + while (i < filter.length() && i < 12) { + char c = filter.charAt(i++); + if (c == '*' || c == '?') { + continue; + } + if (c == ';') { + i--; + break; + } + if ((c < '0' || c > '9') && (c < 'A' || c > 'F')) { + throw new IllegalArgumentException( + "Invalid blacklist address."); + } + } + filter = filter.substring(i); + if (filter.length() == 0) { + return; + } + if (filter.charAt(0) != ';' || filter.length() == 1) { + throw new IllegalArgumentException("Invalid blacklist."); + } + filter = filter.substring(1); + } + } + + /* + * Registers URL within Bluetooth push subsytem. + * + * @param url URL to register + * @throws IOException if an I/O error occurs + */ + public static void registerUrl(String url) throws IOException { + ServiceRecordImpl record = null; + String protocol = url.substring(0, url.indexOf(':')).toUpperCase(); + if (protocol.equals("BTL2CAP")) { + record = L2CAPNotifierImpl.createServiceRecord(url); + } else if (protocol.equals("BTSPP")) { + record = BTSPPNotifierImpl.createServiceRecord(url); + } else if (protocol.equals("BTGOEP")) { + record = BTSPPNotifierImpl.createServiceRecord(url); + } else { + throw new RuntimeException("Unsupported Bluetooth protocol."); + } + record.setHandle(0); + if (!BCC.getInstance().isBluetoothEnabled() && + !BCC.getInstance().enableBluetooth()) { + throw new IOException("Bluetooth radio is not enabled."); + } + if (!registerUrl(url, srs.serialize(record))) { + throw new IOException("Error registering Bluetooth URL."); + } + if (BCC.getInstance().getAccessCode() != DiscoveryAgent.GIAC) { + BCC.getInstance().setAccessCode(DiscoveryAgent.GIAC); + } + // get the emulation services up and running + SDDB.getInstance(); + } + + /* + * Retrieves service record associated with a service maintained by + * Bluetooth push subsytem. + * + * @param notifier Bluetooth notifier to be used with the service record + * @param url URL used during push entry registration + * @return service record instance + */ + public static ServiceRecordImpl getServiceRecord(BluetoothNotifier notifier, + String url) { + return SDDB.getInstance().getServiceRecord(getRecordHandle(url), + notifier); + } + + /* + * Registers URL within Bluetooth push subsystem. + * + * @param url URL to register + * @param data serialized service record created from the URL + * @return true on success, false on failure + */ + private native static boolean registerUrl(String url, byte[] data); + + /* + * Retrieves service record handle for a service maintained by + * Bluetooth push subsytem. + * + * @param url URL used during push entry registration + * @return service record handle + */ + private native static int getRecordHandle(String url); + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/BluetoothStack.java b/java/midp/com/sun/jsr082/bluetooth/BluetoothStack.java new file mode 100644 index 000000000..baca68a47 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/BluetoothStack.java @@ -0,0 +1,680 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; +import javax.bluetooth.BluetoothStateException; +import javax.bluetooth.DeviceClass; +import javax.bluetooth.DiscoveryListener; +import javax.bluetooth.RemoteDevice; + +/* + * Represents native Bluetooth stack provided by the system. + * Provides services such as device and service discovery. + */ +public abstract class BluetoothStack { + + private static class BTInstanceHolder + { + // Note to porting engineer: please replace the class name with + // the one you intend to use on the target platform. + private final static BluetoothStack INSTANCE = new JavacallBluetoothStack(); + } + + /* Instance handle of the native porting layer class. */ + private int nativeInstance = 0; + + /* Listener where discovery events are reported to. */ + private DiscoveryListener discListener = null; + + /* Contains remote name request results. */ + private Hashtable nameResults = new Hashtable(); + + /* Contains authentication request results. */ + private Hashtable authenticateResults = new Hashtable(); + + /* Contains set encryption request results. */ + private Hashtable encryptResults = new Hashtable(); + + /* Timeout value in milliseconds for friendly name retrieval. */ + private final long ASK_FRIENDLY_NAME_TIMEOUT = 0; + + /* Timeout value in milliseconds for authentication. */ + private final long AUTHENTICATE_TIMEOUT = 0; + + /* Timeout value in milliseconds for setting encryption. */ + private final long ENCRYPT_TIMEOUT = 0; + + /* Keeps the count of pending requests. */ + int pollRequests = 0; + + /* + * Contains results of ongoing inquiry to avoid reporting the same + * inquiry result twice. + */ + Vector inquiryHistory = new Vector(); + + /* + * Class constructor. + */ + protected BluetoothStack() { + if (!initialize()) { + throw new RuntimeException( + "Failed to initialize Bluetooth"); + } + } + + /* + * Allocates native resources. + */ + private native boolean initialize(); + + /* + * Releases native resources. + */ + protected native void finalize(); + + /* + * Returns a BluetoothStack object. + * + * @return an instance of BluetoothStack subclass + */ + public synchronized static BluetoothStack getInstance() + { + return BTInstanceHolder.INSTANCE; + } + + /* + * Returns a BluetoothStack object and guarantees that Bluetooth + * radio is on. + * @return an instance of BluetoothStack subclass + * @throws BluetoothStateException if BluetoothStack is off and + * cannot be turned on. + */ + public synchronized static BluetoothStack getEnabledInstance() + throws BluetoothStateException { + BluetoothStack instance = getInstance(); + if (!instance.isEnabled() && !instance.enable()) { + throw new BluetoothStateException("Failed turning Bluetooth on"); + } + // intent here is launching EmulationPolling and SDPServer + // in emulation mode +//???? com.sun.jsr082.bluetooth.SDDB.getInstance(); + return instance; + } + + /* + * Checks if the Bluetooth radio is enabled. + * + * @return true if Bluetooth is enabled, false otherwise + */ + public native boolean isEnabled(); + + /* + * Enables Bluetooth radio. + * + * @return true if Bluetooth is enabled, false otherwise + */ + public native boolean enable(); + + /* + * Returns Bluetooth address of the local device. + * + * @return Bluetooth address of the local device, or null if + * the address could not be retrieved + */ + public native String getLocalAddress(); + + /* + * Returns user-friendly name for the local device. + * + * @return User-friendly name for the local device, or null if + * the name could not be retrieved + */ + public native String getLocalName(); + + /* + * Returns class of device including service classes. + * + * @return class of device value, or -1 if the information could not + * be retrieved + */ + public native int getDeviceClass(); + + /* + * Sets major service class bits of the device. + * + * @param classes an integer whose binary representation indicates the major + * service class bits that should be set + * @return true if the operation succeeded, false otherwise + */ + public native boolean setServiceClasses(int classes); + + /* + * Retrieves the inquiry access code that the local Bluetooth device is + * scanning for during inquiry scans. + * + * @return inquiry access code, or -1 if the information could not + * be retrieved + */ + public native int getAccessCode(); + + /* + * Sets the inquiry access code that the local Bluetooth device is + * scanning for during inquiry scans. + * + * @param accessCode inquiry access code to be set (valid values are in the + * range 0x9e8b00 to 0x9e8b3f), or 0 to take the device out of + * discoverable mode + * @return true if the operation succeeded, false otherwise + */ + public native boolean setAccessCode(int accessCode); + + /* + * Places the device into inquiry mode. + * + * @param accessCode the type of inquiry + * @param listener the event listener that will receive discovery events + * @return true if the inquiry was started, false otherwise + */ + public boolean startInquiry(int accessCode, DiscoveryListener listener) { + if (discListener != null || listener == null) { + return false; + } + discListener = listener; + if (startInquiry(accessCode)) { + inquiryHistory.removeAllElements(); + startPolling(); + return true; + } + return false; + } + + /* + * Removes the device from inquiry mode. + * + * @param listener the listener that is receiving inquiry events + * @return true if the inquiry was canceled, false otherwise + */ + public boolean cancelInquiry(DiscoveryListener listener) { + if (discListener != listener) { + return false; + } + if (cancelInquiry()) { + stopPolling(); + discListener = null; + return true; + } + return false; + } + + /* + * Retrieves friendly name from a remote device synchronously. + * + * @param addr remote device address + * @return friendly name of the remote device, or null + * if the name could not be retrieved + */ + public String askFriendlyNameSync(String addr) { + if (!askFriendlyName(addr)) { + return null; + } + nameResults.remove(addr); + startPolling(); + return (String)waitResult(nameResults, addr, + ASK_FRIENDLY_NAME_TIMEOUT); + } + + /* + * Performs remote device authentication synchronously. + * + * @param addr remote device address + * @return true if authentication was successful, + * false otherwise + */ + public boolean authenticateSync(String addr) { + if (!authenticate(addr)) { + return false; + } + int handle = getHandle(addr); + authenticateResults.remove(new Integer(handle)); + startPolling(); + Boolean result = (Boolean)waitResult(authenticateResults, + new Integer(handle), AUTHENTICATE_TIMEOUT); + if (result == null) { + return false; + } + return result.booleanValue(); + } + + /* + * Sets encryption mode synchronously. + * + * @param addr remote device address + * @param enable true if the encryption needs to be enabled, + * false otherwise + * @return true if authentication was successful, + * false otherwise + */ + public boolean encryptSync(String addr, boolean enable) { + if (!encrypt(addr, enable)) { + return false; + } + int handle = getHandle(addr); + encryptResults.remove(new Integer(handle)); + startPolling(); + Boolean result = (Boolean)waitResult(encryptResults, + new Integer(handle), ENCRYPT_TIMEOUT); + if (result == null) { + return false; + } + return result.booleanValue(); + } + + /* + * Starts a supplementary polling thread. + */ + public synchronized void startPolling() { + pollRequests++; + PollingThread.resume(); + } + + /* + * Cancels event polling for one request. Polling thread will continue to + * run unless there are no other pending requests. + */ + public synchronized void stopPolling() { + pollRequests--; + if (pollRequests > 0) { + return; + } + PollingThread.suspend(); + } + + /* + * Checks for Bluetooth events and processes them. + */ + public void pollEvents() { + while (checkEvents(null)) { + BluetoothEvent event = retrieveEvent(null); + if (event != null) { + event.dispatch(); + } + } + } + + /* + * Retrieves Bluetooth event. + * + * @param handle event handle data + * @return a Bluetooth event object + */ + protected abstract BluetoothEvent retrieveEvent(Object handle); + + /* + * Called when an inquiry request is completed. + * + * @param success indicates whether inquiry completed successfully + */ + void onInquiryComplete(boolean success) { + if (discListener == null) { + return; + } + stopPolling(); + discListener = null; + inquiryHistory.removeAllElements(); + int type = success ? DiscoveryListener.INQUIRY_COMPLETED : + DiscoveryListener.INQUIRY_ERROR; + DiscoveryAgentImpl.getInstance().inquiryCompleted(type); + } + + /* + * Called when an inquiry result is obtained. + * + * @param result inquiry result object + */ + void onInquiryResult(InquiryResult result) { + if (discListener == null) { + return; + } + String addr = result.getAddress(); + Enumeration e = inquiryHistory.elements(); + while (e.hasMoreElements()) { + InquiryResult oldResult = (InquiryResult)e.nextElement(); + if (oldResult.getAddress().equals(addr)) { + // inquiry result is already in our possession + return; + } + } + inquiryHistory.addElement(result); + RemoteDevice dev + = DiscoveryAgentImpl.getInstance().getRemoteDevice(addr); + DiscoveryAgentImpl.getInstance().addCachedDevice(addr); + discListener.deviceDiscovered(dev, result.getDeviceClass()); + } + + /* + * Called when a name retrieval request is completed. + * + * @param addr Bluetooth address of a remote device + * @param name friendly name of the device + */ + void onNameRetrieve(String addr, String name) { + stopPolling(); + putResult(nameResults, addr, name); + } + + /* + * Called when an authentication request is completed. + * + * @param handle connection handle for an ACL connection + * @param result indicates whether the operation was successful + */ + void onAuthenticationComplete(int handle, boolean result) { + stopPolling(); + putResult(authenticateResults, new Integer(handle), + new Boolean(result)); + } + + /* + * Called when a set encryption request is completed. + * + * @param handle connection handle for an ACL connection + * @param result indicates whether the operation was successful + */ + void onEncryptionChange(int handle, boolean result) { + stopPolling(); + putResult(encryptResults, new Integer(handle), new Boolean(result)); + } + + /* + * Puts result value into hastable and notifies threads waiting for the + * result to appear. + * + * @param hashtable Hashtable object where the result will be + * stored + * @param key key identifying the result + * @param value value of the result + */ + private void putResult(Hashtable hashtable, Object key, Object value) { + synchronized (hashtable) { + hashtable.put(key, value); + hashtable.notify(); + } + } + + /* + * Waits for the specified key to appear in the given hastable. If the key + * does not appear within the timeout specified, null value is + * returned. + * + * @param hashtable Hashtable object where the key is expected + * @param key the key expected to appear in the hastable + * @param timeout timeout value in milliseconds + * @return Object corresponding to the given key + */ + private Object waitResult(Hashtable hashtable, Object key, long timeout) { + synchronized (hashtable) { + if (timeout == 0) { + // infinite timeout + while (true) { + if (hashtable.containsKey(key)) { + return hashtable.remove(key); + } + try { + // wait for a new key-value pair to appear in hashtable + hashtable.wait(); + } catch (InterruptedException e) { + return null; + } + } + } + // endTime indicates time up to which the method is allowed to run + long endTime = System.currentTimeMillis() + timeout; + while (true) { + if (hashtable.containsKey(key)) { + return hashtable.remove(key); + } + // update timeout value + timeout = endTime - System.currentTimeMillis(); + if (timeout <= 0) { + return null; + } + try { + // wait for a new key-value pair to appear in hashtable + hashtable.wait(timeout); + } catch (InterruptedException e) { + return null; + } + } + } + } + + /* + * Retrieves default ACL connection handle for the specified remote device. + * + * @param addr the Bluetooth address of the remote device + * @return ACL connection handle value + */ + private native int getHandle(String addr); + + /* + * Passes device discovery request to the native porting layer. + * + * @param accessCode the type of inquiry + * @return true if the operation was accepted, + * false otherwise + */ + private native boolean startInquiry(int accessCode); + + /* + * Passes cancellation of device discovery request to the native porting + * layer. + * + * @return true if the operation was accepted, + * false otherwise + */ + private native boolean cancelInquiry(); + + /* + * Passes remote device's friendly name acquisition request to the native + * porting layer. + * + * @param addr Bluetooth address of the remote device + * @return true if the operation was accepted, + * false otherwise + */ + private native boolean askFriendlyName(String addr); + + /* + * Passes remote device authentication request to the native porting layer. + * + * @param addr Bluetooth address of the remote device + * @return true if the operation was accepted, + * false otherwise + */ + private native boolean authenticate(String addr); + + /* + * Passes connection encryption change request to the native porting layer. + * + * @param addr Bluetooth address of the remote device + * @param enable true if the encryption needs to be enabled, + * false otherwise + * @return true if the operation was accepted, + * false otherwise + */ + private native boolean encrypt(String addr, boolean enable); + + /* + * Checks if Bluetooth events are available on the native porting layer. + * + * @param handle interger two words length array for storing event data in; + * the first word indicates event's minor id. + * @return true if there are pending events, + * false otherwise + */ + protected native boolean checkEvents(Object handle); + + /* + * Reads binary event data from the native porting layer. This data can + * be interpreted differently by different subclasses of BluetoothStack. + * + * @param eventData event object to be filled with data + * @return number of bytes read + */ + protected native boolean readData(Object eventData); +} + +/* + * Supplementary thread which periodically polls Bluetooth stack for events. + */ +class PollingThread extends Thread { + + /* Instance of this class. */ + private static PollingThread instance = new PollingThread(); + + /* Flag indicating if this thread should be suspended. */ + private static boolean suspended = true; + + /* Polling interval in milliseconds. */ + private final int POLL_INTERVAL = 1000; + + /* + * Class constructor. + */ + public PollingThread() { + } + + /* + * Suspends this thread. + */ + public static void suspend() { + synchronized (instance) { + suspended = true; + } + } + + /* + * Resumes this thread. + */ + public static void resume() { + try { + instance.start(); + } catch (IllegalThreadStateException e) { + } + synchronized (instance) { + suspended = false; + instance.notify(); + } + } + + /* + * Execution body. + */ + public void run() { + BluetoothStack stack = BluetoothStack.getInstance(); + while (true) { + try { + synchronized (this) { + if (suspended) { + wait(); + } + } + stack.pollEvents(); + synchronized (this) { + wait(POLL_INTERVAL); + } + } catch (InterruptedException e) { + break; + } + } + } + +} + +/* + * Supplementary thread which dispatches Bluetooth events to user notifiers. + */ +class Dispatcher extends Thread { + + /* Instance of this class. */ + private static Dispatcher instance = new Dispatcher(); + + /* Vector containing Bluetooth events awaiting to be dispatched. */ + private static Vector events = new Vector(); + + /* + * Class constructor. + */ + public Dispatcher() { + } + + /* + * Puts the event in the event queue. + * It also starts event dispatcher thread if it's not running yet. + * + * @param event the event to be enqueued + */ + public static void enqueue(BluetoothEvent event) { + try { + instance.start(); + } catch (IllegalThreadStateException e) { + } + synchronized (events) { + events.addElement(event); + events.notify(); + } + } + + /* + * Execution body. + */ + public void run() { + while (true) { + BluetoothEvent event = null; + synchronized (events) { + if (!events.isEmpty()) { + event = (BluetoothEvent)events.firstElement(); + events.removeElementAt(0); + } else { + try { + events.wait(); + } catch (InterruptedException e) { + break; + } + } + } + if (event != null) { + event.process(); + } + } + } + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/BluetoothUrl.java b/java/midp/com/sun/jsr082/bluetooth/BluetoothUrl.java new file mode 100644 index 000000000..cb26c9b1d --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/BluetoothUrl.java @@ -0,0 +1,599 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import javax.bluetooth.UUID; +import javax.bluetooth.BluetoothConnectionException; +import java.util.Hashtable; + +/* + * Represents a bluetooth url, i.e. connection string. + * There are two ways of usage. First one is constructing it giving + * url string in order to parse it into a set of fields. Second one + * is constructig it giving fields values in order to get string + * representation. Whenever incompatible url parts are found + * IllegalArgumentException is thrown. + */ +public class BluetoothUrl { + /* Indicates if it is a sever connection string. */ + public boolean isServer = false; + + /* Keeps server address for client url, "localhost" for server. */ + public String address = null; + /* PSM for L2CAP or channel id for RFCOMM. */ + public int port = -1; + /* Master parameter, true by default for server. */ + public boolean master = false; + /* Encrypt parameter. */ + public boolean encrypt = false; + /* Authenticate parameter. */ + public boolean authenticate = false; + + /* Value to indicate L2CAP protocol. */ + public static final int L2CAP = 0; + /* Value to indicate RFCOMM protocol. */ + public static final int RFCOMM = 1; + /* Value to indicate OBEX protocol. */ + public static final int OBEX = 2; + /* Value to indicate unknown protocol. */ + public static final int UNKNOWN = 3; + /* Indicates protocol type. */ + public int protocol = UNKNOWN; + + /* Amount of protocols supported. */ + private static final int PROTOCOLS_AMOUNT = 3; + /* + * Keeps protocols indicating strings. + * Usage: + * protocolName[L2CAP] to get "l2cap" + */ + private static final String[] protocolName = + { "btl2cap://", "btspp://", "btgoep://" }; + + /* + * Keeps uuid from server connection string, + * null for client's one. + * L2CAP, RFCOMM specific. + */ + public String uuid = null; + + /* + * Name parameter of server url, null for client's one. + * L2CAP, RFCOMM specific. + */ + public String name = null; + + /* Url string to parse, lower case. */ + private String url; + /* + * Url string to parse, original case. + * Required for correct "name" parameter parsing for it is case-sensitive. + */ + public String caseSensitiveUrl; + + /* Authorize parameter. L2CAP specific. */ + public boolean authorize = false; + /* RecieveMTU parameter. L2CAP specific. */ + public int receiveMTU = -1; + /* TransmitMTU parameter. L2CAP specific. */ + public int transmitMTU = -1; + + /* UUID value to create a transport for Service Discovery Protocol. */ + public static final UUID UUID_SDP = new UUID(0x0001); + + /* Indicates if an explicit "authenticate" parameter found. */ + private boolean explicitAuthenticate = false; + + /* Keeps server host string. */ + private static final String LOCALHOST = "localhost"; + + /* Keeps length of url. */ + private int length = 0; + + /* Master parameter name. */ + private static final String MASTER = ";master="; + /* Encrypt parameter name. */ + private static final String ENCRYPT = ";encrypt="; + /* Authenticate parameter name. */ + private static final String AUTHENTICATE = ";authenticate="; + /* Authorize parameter name. */ + private static final String AUTHORIZE = ";authorize="; + /* TransmitMTU parameter name. */ + private static final String TRANSMITMTU = ";transmitmtu="; + /* ReceiveMTU parameter name. */ + private static final String RECEIVEMTU = ";receivemtu="; + /* Name parameter name. */ + private static final String NAME = ";name="; + + /* "true" literal. */ + private static final String TRUE = "true"; + /* "false" literal. */ + private static final String FALSE = "false"; + + /* the URL parameters. */ + private Hashtable parameters; + + /* Stub object for values in parameters hashtable.*/ + private final static Object on = new Object(); + + /* Shows whether this url is generated and validated by SDP routines. */ + private boolean isSystem = false; + + /* + * Constructs url object by specified url string. Constructing + * BluetoothUrl in this manner is a way to parse + * an url represented by string. + * + * @param urlString url string. + */ + public BluetoothUrl(String urlString) { + this(UNKNOWN, urlString, null); + } + + /* + * Constructs url object by specified protocol and url string without + * leading protocol name and colon or if protocol is unknown by s string + * that contains full url. + * + * @param protocol prootocol type, must be one of + * L2CAP, RFCOMM, OBEX, UNKNOWN. + * @param urlString whole url if protocol value is + * UNKNOWN, a part of url string beyond + * "protocol:" otherwise. + */ + public BluetoothUrl(int protocol, String urlString) { + this(protocol, urlString, null); + } + + /* + * Constructs url object with specified protocol, url and special system + * token. + * @see BluetoothUrl(int, String) + * + * @param protocol prootocol type + * @param urlString URL + * @param systemToken special object that validates this URL as system + * if has proper value, usually it is null + */ + public BluetoothUrl(int protocol, String urlString, Object systemToken) { + this(protocol); + + isSystem = SDP.checkSystemToken(systemToken); + caseSensitiveUrl = urlString; + url = urlString.toLowerCase(); + length = url.length(); + int start; + int separator = url.indexOf(':'); + + if (protocol == UNKNOWN) { + // url is "PROTOCOL://ADDRESS:...", parsing protocol name + assertTrue(separator > 0, "Cannot parse protocol name: " + url); + start = separator + 3; // skip "://" + String name = urlString.substring(0, start); + + for (int i = 0; i < PROTOCOLS_AMOUNT; i++) { + if (protocolName[i].equals(name)) { + this.protocol = i; + separator = url.indexOf(':', start); + break; + } + } + + } else { + // url is "//ADDRESS:...", parsing protocol name + assertTrue(urlString.startsWith("//"), + "address and protocol name have to be separated by //: " + url); + // skip "//" + start = 2; + } + + assertTrue(separator > start, "Cannot parse address: " + url); + + address = url.substring(start, separator); + start = separator + 1; + + + if (this.protocol == L2CAP) { + // parsing psm or uuid + if (address.equals(LOCALHOST)) { + isServer = true; + // Now uuid goes till end of string or semicolon. + separator = getSeparator(start); + uuid = url.substring(start, separator); + + } else { + // Now psm goes which is represented by 4 hex digits. + assertTrue((separator = start + 4) <= length, + "psm has to be represented by 4 hex digits: " + url); + port = parseInt(start, separator, 16); + } + + } else if (this.protocol == RFCOMM || + this.protocol == OBEX) { + separator = getSeparator(start); + if (address.equals(LOCALHOST)) { + isServer = true; + // Now uuid goes till end of string or semicolon. + uuid = url.substring(start, separator); + } else { + // Now channel id goes which is represented by %d1-30. + assertTrue(separator <= length, + "channel id has to go after address: " + url); + port = parseInt(start, separator, 10); + } + } else { + separator = getSeparator(start); + port = parseInt(start, separator, 16); + } + + if (isServer) { + int length; + assertTrue(uuid != null && (length = uuid.length()) > 0 && + length <= 32, "Invalid UUID"); + } else { + checkBluetoothAddress(); + } + + // parsing parameters + parameters = new Hashtable(); + for (start = separator; start < length; start = parseParameter(start)); + parameters = null; + + assertTrue(start == length, "Cannot parse the parameters: " + url); + } + + /* + * Creates url that represents client connection string. + * + * @param protocol identifies protocol. Should be one of + * + * BluetoothUrl.L2CAP, BluetoothUrl.RFCOMM, BluetoothUrl.OBEX + * + * + * @param btaddr Bluetooth address of server device. + * + * @param port PSM in case of L2CAP or channel id otherwise. + * + * @return BluetoothUrl instance that represents + * desired connection string. + * + * @exception IllegalArgument exception if provived parameters are invalid. + */ + public static BluetoothUrl createClientUrl(int protocol, + String btaddr, int port) + throws IllegalArgumentException { + + assertTrue(protocol != UNKNOWN && btaddr != null, + "Either unknown protocol name or address"); + BluetoothUrl url = new BluetoothUrl(protocol); + + url.address = btaddr.toLowerCase(); + url.checkBluetoothAddress(); + url.port = port; + + return url; + } + + /* + * Universal private constructor. + * @param protocol identifies protocol. + * @exception IllegalArgument exception if provived parameters are invalid. + */ + private BluetoothUrl(int protocol) { + assertTrue(protocol <= UNKNOWN, "Unknown protocol name: " + protocol); + this.protocol = protocol; + } + + /* + * Checks url parts consistency and creates string representation. + * @return string representation of the URL. + * @exception IllegalArgumentException if URL parts are inconsistent. + */ + public String toString() { + assertTrue(protocol == L2CAP || + protocol == RFCOMM || + protocol == OBEX, + "Incorrect protocol bname: " + protocol); + + StringBuffer buffer = new StringBuffer(); + + buffer = new StringBuffer(getResourceName()); + buffer.append(':'); + + if (isServer) { + buffer.append(uuid); + buffer.append(AUTHORIZE).append(authorize ? TRUE : FALSE); + } else { + String portStr; + + if (protocol == L2CAP) { + // in case of l2cap, the psm is 4 hex digits + portStr = Integer.toHexString(port); + for (int pad = 4 - portStr.length(); pad > 0; pad--) { + buffer.append('0'); + } + + } else if (protocol == RFCOMM || + protocol == OBEX) { + portStr = Integer.toString(port); + } else { + portStr = Integer.toString(port); + } + + buffer.append(portStr); + } + + /* + * note: actually it's not required to add the boolean parameter if it + * equals to false because if it is not present in the connection + * string, this is equivalent to 'such parameter'=false. + * But some TCK tests check the parameter is always present in + * URL string even its value is false. + * IMPL_NOTE: revisit this code if TCK changes. + */ + buffer.append(MASTER).append(master ? TRUE : FALSE); + buffer.append(ENCRYPT).append(encrypt ? TRUE: FALSE); + buffer.append(AUTHENTICATE).append(authenticate ? TRUE : FALSE); + + if (receiveMTU != -1) { + buffer.append(RECEIVEMTU).append( + Integer.toString(receiveMTU, 10)); + } + if (transmitMTU != -1) { + buffer.append(TRANSMITMTU).append( + Integer.toString(transmitMTU, 10)); + } + + return buffer.toString(); + } + + /* + * Creates string representation of the URL without parameters. + * @return "PROTOCOL://ADDRESS" string. + */ + public String getResourceName() { + assertTrue(protocol == L2CAP || + protocol == RFCOMM || + protocol == OBEX, + "Incorrect protocol bname: " + protocol); + assertTrue(address != null, "Incorrect address: "+ address); + return protocolName[protocol] + address; + } + + /* + * Tests if this URL is system one. System URL can only by created + * by SDP server or client and is processed in special way. + * + * @return true if this url is a system one created + * by SDP routines, false otherwise + */ + public final boolean isSystem() { + return isSystem; + } + + /* + * Checks the string given is a valid Bluetooth address, which means + * consists of 12 hexadecimal digits. + * + * @exception IllegalArgumentException if string given is not a valid + * Bluetooth address + */ + private void checkBluetoothAddress() + throws IllegalArgumentException { + + String errorMessage = "Invalid Bluetooth address"; + assertTrue(address != null && address.length() == 12 && + address.indexOf('-') == -1, errorMessage); + + try { + Long.parseLong(address, 16); + } catch (NumberFormatException e) { + assertTrue(false, errorMessage); + } + } + + /* + * Parses parameter in url starting at given position and cheks simple + * rules or parameters compatibility. Parameter is ";NAME=VALUE". If + * parsing from given position or a check fails, + * IllegalArgumentException is thrown. + * + * @param start position to start parsing at, if it does not point to + * semicolon, parsing fails as well as instance constructing. + * @return position number that immediately follows parsed parameter. + * @exception IllegalArgumentException if parsing fails or incompatible + * parameters occured. + */ + private int parseParameter(int start) throws IllegalArgumentException { + assertTrue(url.charAt(start) == ';', + "Cannot parse url parameters: " + url); + + int separator = url.indexOf('=', start) + 1; + assertTrue(separator > 0, "Cannot parse url parameters: " + url); + // name is ";NAME=" + String name = url.substring(start, separator); + + start = separator; + separator = getSeparator(start); + + assertTrue(!parameters.containsKey(name), + "Duplicate parameter " + name); + parameters.put(name, on); + + if (name.equals(MASTER)) { + master = parseBoolean(start, separator); + + } else if (name.equals(ENCRYPT)) { + encrypt = parseBoolean(start, separator); + if (encrypt && !explicitAuthenticate) { + authenticate = true; + } + + } else if (name.equals(AUTHENTICATE)) { + authenticate = parseBoolean(start, separator); + explicitAuthenticate = true; + + } else if (name.equals(NAME)) { + assertTrue(isServer, "Incorrect parameter for client: " + name); + // this parameter is case-sensitive + this.name = caseSensitiveUrl.substring(start, separator); + assertTrue(checkNameFormat(this.name), + "Incorrect name format: " + this.name); + + } else if (name.equals(AUTHORIZE)) { + assertTrue(isServer, "Incorrect parameter for client: " + name); + authorize = parseBoolean(start, separator); + if (authorize && !explicitAuthenticate) { + authenticate = true; + } + + } else if (protocol == L2CAP) { + if (name.equals(RECEIVEMTU)) { + receiveMTU = parseInt(start, separator, 10); + assertTrue(receiveMTU > 0, + "Incorrect receive MTU: " + receiveMTU); + } else if (name.equals(TRANSMITMTU)) { + transmitMTU = parseInt(start, separator, 10); + assertTrue(transmitMTU > 0, + "Incorrect transmit MTU: " + transmitMTU); + } else { + assertTrue(false, "Unknown parameter name = " + name); + } + } else { + assertTrue(false, "Unknown parameter name = " + name); + } + return separator; + } + + /* + * Checks name format. + * name = 1*( ALPHA / DIGIT / SP / "-" / "_") + * The core rules from RFC 2234. + * + * @param name the name + * @return true if the name format is valid, + * false otherwise + */ + private boolean checkNameFormat(String name) { + char[] a = name.toCharArray(); + boolean ret = a.length > 0; + for (int i = a.length; --i >= 0 && ret;) { + ret &= (a[i] >= 'a' && a[i] <= 'z') || + (a[i] >= 'A' && a[i] <= 'Z') || + (a[i] >= '0' && a[i] <= '9') || + (a[i] == '-') || + (a[i] == '_') || + (a[i] == ' '); + } + return ret; + } + + /* + * Retrieves position of semicolon in the rest of url string, returning + * length of the string if no semicolon found. It also assertd that a + * non-empty substring starts from start given and ends + * before semicolon or end of string. + * + * @param start position in url string to start searching at. + * + * @return position of first semicolon or length of url string if there + * is no semicolon. + * + * @exception IllegalArgumentException if there is no non-empty substring + * before semicolon or end of url. + */ + private int getSeparator(int start) { + int separator = url.indexOf(';', start); + if (separator < 0) { + separator = length; + } + assertTrue(start < separator, "Correct separator is not found"); + + return separator; + } + + /* + * Parses boolean value from url string. + * + * @param start position to start parsing from. + * @param separator position that immediately follows value to parse. + * + * @return true if, comparing case insensitive, specified substring is + * "TRUE", false if it is "FALSE". + * @exception IllegalArgumentException if specified url substring is + * neither "TRUE" nor "FALSE", case-insensitive. + */ + private boolean parseBoolean(int start, int separator) + throws IllegalArgumentException { + String value = url.substring(start, separator); + if (value.equals(TRUE)) { + return true; + } + + assertTrue(value.equals(FALSE), "Incorrect boolean parsing: " + value); + return false; + } + + /* + * Parses integer value from url string. + * + * @param start position to start parsing from. + * @param separator position that immediately follows value to parse. + * @param radix the radix to use. + * + * @return integer value been parsed. + * @exception IllegalArgumentException if given string is not + * case-insensitive "TRUE" or "FALSE". + */ + private int parseInt(int start, int separator, int radix) + throws IllegalArgumentException { + int result = -1; + + try { + result = Integer.parseInt( + url.substring(start, separator), radix); + } catch (NumberFormatException e) { + assertTrue(false, "Incorrect int parsing: " + + url.substring(start, separator)); + } + + return result; + } + + /* + * Asserts that given condition is true. + * @param condition condition to check. + * @param details condition's description details. + * @exception IllegalArgumentException if given condition is flase. + */ + private static void assertTrue(boolean condition, String details) + throws IllegalArgumentException { + if (!condition) { + throw new IllegalArgumentException("unexpected parameter: " + + details); + } + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/BluetoothUtils.java b/java/midp/com/sun/jsr082/bluetooth/BluetoothUtils.java new file mode 100644 index 000000000..27d39dfd6 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/BluetoothUtils.java @@ -0,0 +1,93 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; + +/* + * Class contains Bluetooth helper methods. + */ +public class BluetoothUtils { + /* Size of byte representation of Bluetooth address. */ + public static final int BTADDR_SIZE = 6; + + /* + * Converts Bluetooth address from byte array to string representation. + * + * @param btaddr 6-bytes byte array containing Bluetooth address in + * Bluetooth byte order (little-endian) + * @return Bluetooth address as string in + * user-friendly byte order (big-endian) without comma separator + * @throws IllegalArgumentException if input address is invalid + */ + public static String getAddressString(byte[] btaddr) + throws IllegalArgumentException { + final int len = btaddr.length; + if (len != BTADDR_SIZE) { + throw new IllegalArgumentException("Incorrect address size"); + } + + StringBuffer sb = new StringBuffer(len * 2); + String s; + for (int i = (len - 1); i >= 0; i--) { + // convert decimal to hexadecimal with leading zeroes and uppercase + s = Integer.toHexString((btaddr[i] >> 4) & 0xF); + sb.append(s.toUpperCase()); + s = Integer.toHexString(btaddr[i] & 0xF); + sb.append(s.toUpperCase()); + } + + return sb.toString(); + } + + /* + * Converts Bluetooth address from string to byte array representation. + * + * @param btaddr Bluetooth address as string in + * user-friendly byte order (big-endian) without comma separator + * @return 6-bytes byte array containing Bluetooth address in + * Bluetooth byte order (little-endian) + * @throws IllegalArgumentException if input address is invalid + */ + public static byte[] getAddressBytes(String btaddr) + throws IllegalArgumentException { + final int len = btaddr.length() / 2; + if (len != BTADDR_SIZE) { + throw new IllegalArgumentException("Incorrect address size"); + } + + byte[] bytes = new byte[len]; + try { + for (int i = 0; i < len; i++) { + String s = btaddr.substring(i * 2, i * 2 + 2); + bytes[len - 1 - i] = (byte)Integer.parseInt(s, 16); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Incorrect address value"); + } + + return bytes; + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/ClientServiceAttributeTransaction.java b/java/midp/com/sun/jsr082/bluetooth/ClientServiceAttributeTransaction.java new file mode 100644 index 000000000..8de749a4b --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/ClientServiceAttributeTransaction.java @@ -0,0 +1,121 @@ +/* + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import java.io.IOException; +import java.util.Enumeration; + +import javax.bluetooth.DataElement; + +/* + * Provides ServiceAttribute transaction functionality. + */ +public class ClientServiceAttributeTransaction extends SDPClientTransaction { + + /* ServiceRecordHandle (BT Spec 1.2 Vol 3 page 138). */ + int serviceRecordHandle; + /* AttributeIDList (BT Spec 1.2 Vol 3 page 139). */ + DataElement attributeIDList; + /* AttributeList (BT Spec 1.2 Vol 3 page 140). */ + byte[] attributes; + + /* + * Constructs ServiceAttributeTransaction object. + */ + ClientServiceAttributeTransaction(JavaSDPClient client, int transactionID, + SDPResponseListener listener, int recordHandle, + int[] attrSet) { + super(client, SDPClientTransaction.SDP_SERVICE_ATTRIBUTE_REQUEST, transactionID, listener); + serviceRecordHandle = recordHandle; + attributeIDList = new DataElement(DataElement.DATSEQ); + for (int i = 0; i < attrSet.length; i++) { + attributeIDList.addElement(new DataElement( + DataElement.U_INT_2, attrSet[i])); + } + parameterLength = super.client.getConnection().getReaderWriter().getDataSize(attributeIDList) + 6; + } + + /* + * Writes transaction-specific parameters into the PDU. + * + * @throws IOException when an I/O error occurs + */ + void writeParameters() throws IOException { + super.client.getConnection().getReaderWriter().writeInteger(serviceRecordHandle); + super.client.getConnection().getReaderWriter().writeShort((short)MAX_ATTRIBUTE_BYTE_COUNT); + super.client.getConnection().getReaderWriter().writeDataElement(attributeIDList); + } + + /* + * Reads transaction-specific parameters from the PDU. + * + * @param length length of PDU's parameters + * @throws IOException when an I/O error occurs + */ + void readParameters(int length) throws IOException { + short byteCount = super.client.getConnection().getReaderWriter().readShort(); + byte[] data = super.client.getConnection().getReaderWriter().readBytes(byteCount); + if (attributes == null) { + attributes = data; + } else { + byte[] temp = attributes; + attributes = new byte[temp.length + byteCount]; + System.arraycopy(temp, 0, attributes, 0, temp.length); + System.arraycopy(data, 0, attributes, temp.length, + byteCount); + } + } + + /* + * Completes the transaction by calling corresponding listener's + * method with the data retrieved. + */ + public void complete() { + DataElement attrList; + DataElementSerializer des = new DataElementSerializer(); + try { + attrList = des.restore(attributes); + } catch (IOException e) { + listener.serviceAttributeResponse(null, null, + ssTransID); + return; + } + int size = attrList.getSize() / 2; + if (size == 0) { + listener.serviceAttributeResponse(null, null, + ssTransID); + return; + } + Enumeration elements = (Enumeration)attrList.getValue(); + final int[] attrIDs = new int[size]; + final DataElement[] attrValues = new DataElement[size]; + for (int i = 0; elements.hasMoreElements(); i++) { + attrIDs[i] = (int)((DataElement) + elements.nextElement()).getLong(); + attrValues[i] = ((DataElement) + elements.nextElement()); + } + listener.serviceAttributeResponse(attrIDs, attrValues, ssTransID); + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/ClientServiceSearchAttributeTransaction.java b/java/midp/com/sun/jsr082/bluetooth/ClientServiceSearchAttributeTransaction.java new file mode 100644 index 000000000..eef1144e9 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/ClientServiceSearchAttributeTransaction.java @@ -0,0 +1,140 @@ +/* + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import java.io.IOException; +import java.util.Enumeration; + +import javax.bluetooth.DataElement; +import javax.bluetooth.UUID; + +/* + * Provides ServiceSearchAttribute transaction functionality. + */ +public class ClientServiceSearchAttributeTransaction extends SDPClientTransaction { + +/* ServiceSearchPattern (BT Spec 1.2 Vol 3 page 142). */ +DataElement uuidData; +/* AttributeIDList (BT Spec 1.2 Vol 3 page 142). */ +DataElement attrData; +/* AttributeLists (BT Spec 1.2 Vol 3 page 143). */ +byte[] attributes; + +/* + * Constructs ServiceSearchAttributeTransaction object. + */ +public ClientServiceSearchAttributeTransaction(JavaSDPClient client, int transactionID, + SDPResponseListener listener, int[] attrSet, + UUID[] uuidSet) { + super(client, SDPClientTransaction.SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST, transactionID, + listener); + attrData = new DataElement(DataElement.DATSEQ); + uuidData = new DataElement(DataElement.DATSEQ); + for (int i = 0; i < attrSet.length; i++) { + attrData.addElement(new DataElement(DataElement.U_INT_2, + attrSet[i])); + } + for (int i = 0; i < uuidSet.length; i++) { + uuidData.addElement(new DataElement(DataElement.UUID, + uuidSet[i])); + } + parameterLength = super.client.getConnection().getReaderWriter().getDataSize(attrData) + + super.client.getConnection().getReaderWriter().getDataSize(uuidData) + 2; +} + +/* + * Writes transaction-specific parameters into the PDU. + * + * @throws IOException + * when an I/O error occurs + */ +void writeParameters() throws IOException { + super.client.getConnection().getReaderWriter().writeDataElement(uuidData); + super.client.getConnection().getReaderWriter().writeShort((short)MAX_ATTRIBUTE_BYTE_COUNT); + super.client.getConnection().getReaderWriter().writeDataElement(attrData); +} + +/* + * Reads transaction-specific parameters from the PDU. + * + * @param length + * length of PDU's parameters + * @throws IOException + * when an I/O error occurs + */ +void readParameters(int length) throws IOException { + short byteCount = super.client.getConnection().getReaderWriter().readShort(); + byte[] data = super.client.getConnection().getReaderWriter().readBytes(byteCount); + if (attributes == null) { + attributes = data; + } else { + byte[] temp = attributes; + attributes = new byte[temp.length + byteCount]; + System.arraycopy(temp, 0, attributes, 0, temp.length); + System.arraycopy(data, 0, attributes, temp.length, + byteCount); + } +} + +/* + * Completes the transaction by calling corresponding listener's method with the + * data retrieved. + */ +public void complete() { + DataElement attrList; + DataElementSerializer des = new DataElementSerializer(); + try { + Enumeration attributeLists = (Enumeration)des. + restore(attributes).getValue(); + if (attributeLists.hasMoreElements()) { + attrList = (DataElement)attributeLists.nextElement(); + } else { + listener.serviceSearchAttributeResponse(null, null, + ssTransID); + return; + } + } catch (IOException e) { + listener.serviceSearchAttributeResponse(null, null, + ssTransID); + return; + } + int size = attrList.getSize() / 2; + if (size == 0) { + listener.serviceSearchAttributeResponse(null, null, + ssTransID); + return; + } + Enumeration elements = (Enumeration)attrList.getValue(); + int[] attrIDs = new int[size]; + DataElement[] attrValues = new DataElement[size]; + for (int i = 0; elements.hasMoreElements(); i++) { + attrIDs[i] = (int)((DataElement) + elements.nextElement()).getLong(); + attrValues[i] = ((DataElement) + elements.nextElement()); + } + listener.serviceSearchAttributeResponse(attrIDs, attrValues, + ssTransID); +} +} diff --git a/java/midp/com/sun/jsr082/bluetooth/ClientServiceSearchTransaction.java b/java/midp/com/sun/jsr082/bluetooth/ClientServiceSearchTransaction.java new file mode 100644 index 000000000..7a6c88cd9 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/ClientServiceSearchTransaction.java @@ -0,0 +1,93 @@ +/* + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import java.io.IOException; + +import javax.bluetooth.DataElement; +import javax.bluetooth.UUID; + +/* + * Provides ServiceSearch transaction functionality. + */ +public class ClientServiceSearchTransaction extends SDPClientTransaction { + + /* ServiceSearchPattern (BT Spec 1.2 Vol 3 page 135). */ + DataElement serviceSearchPattern; + /* Acquired service record handles. */ + int[] handleList; + /* Current position in the handleList. */ + int offset; + + /* + * Constructs ServiceSearchTransaction object. + */ + public ClientServiceSearchTransaction(JavaSDPClient client, int transactionID, + SDPResponseListener listener, UUID[] uuidSet) { + super(client, SDPClientTransaction.SDP_SERVICE_SEARCH_REQUEST, transactionID, listener); + serviceSearchPattern = new DataElement(DataElement.DATSEQ); + for (int i = 0; i < uuidSet.length; i++) { + serviceSearchPattern.addElement(new DataElement( + DataElement.UUID, uuidSet[i])); + } + parameterLength = super.des.getDataSize(serviceSearchPattern) + 2; + } + + /* + * Writes transaction-specific parameters into the PDU. + * + * @throws IOException when an I/O error occurs + */ + void writeParameters() throws IOException { + super.client.getConnection().getReaderWriter().writeDataElement(serviceSearchPattern); + super.client.getConnection().getReaderWriter().writeShort((short)MAX_SERVICE_RECORD_COUNT); + } + + /* + * Reads transaction-specific parameters from the PDU. + * + * @param length length of PDU's parameters + * @throws IOException when an I/O error occurs + */ + void readParameters(int length) throws IOException { + int totalServiceRecordCount = super.client.getConnection().getReaderWriter().readShort(); + int currentServiceRecordCount = super.client.getConnection().getReaderWriter().readShort(); + if (handleList == null && totalServiceRecordCount > 0) { + handleList = new int[totalServiceRecordCount]; + } + for (int i = 0; i < currentServiceRecordCount; i++) { + handleList[offset] = super.client.getConnection().getReaderWriter().readInteger(); + offset++; + } + System.out.println("<<< " + totalServiceRecordCount + " ServiceRecords found" ); + } + + /* + * Completes the transaction by calling corresponding listener's + * method with the data retrieved. + */ + public void complete() { + listener.serviceSearchResponse(handleList, ssTransID); + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/DataElementSerializer.java b/java/midp/com/sun/jsr082/bluetooth/DataElementSerializer.java new file mode 100644 index 000000000..635822981 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/DataElementSerializer.java @@ -0,0 +1,629 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Stack; +import javax.bluetooth.DataElement; +import javax.bluetooth.ServiceRecord; +import javax.bluetooth.UUID; + +/* + * Serializes and restores DataElement objects. + */ +public class DataElementSerializer { + /* NULL data header. */ + private static final byte NULL_DATA = 0x00; + + /* Boolean data header. */ + private static final byte BOOLEAN_DATA = 0x28; + + /* 1-byte signed integer header. */ + private static final byte INT1_SIGNED = 0x10; + + /* 2-byte signed integer header. */ + private static final byte INT2_SIGNED = 0x11; + + /* 4-byte signed integer header. */ + private static final byte INT4_SIGNED = 0x12; + + /* 8-byte signed integer header. */ + private static final byte INT8_SIGNED = 0x13; + + /* 16-byte signed integer header. */ + private static final byte INT16_SIGNED = 0x14; + + /* 1-byte unsigned integer header. */ + private static final byte INT1_UNSIGNED = 0x08; + + /* 2-byte unsigned integer header. */ + private static final byte INT2_UNSIGNED = 0x09; + + /* 4-byte unsigned integer header. */ + private static final byte INT4_UNSIGNED = 0x0a; + + /* 8-byte unsigned integer header. */ + private static final byte INT8_UNSIGNED = 0x0b; + + /* 16-byte unsigned integer header. */ + private static final byte INT16_UNSIGNED = 0x0c; + + /* 16-bit UUID header. */ + private static final byte UUID_2 = 0x19; + + /* 32-bit UUID header. */ + private static final byte UUID_4 = 0x1a; + + /* 128-bit UUID header. */ + private static final byte UUID_16 = 0x1c; + + /* Mask to get type tag from header. */ + private static final byte TYPE_MASK = ((byte)0xf8); + + /* Mask to get size of data size field from header. */ + private static final byte SIZE_MASK = 0x07; + + /* Tag for string type. */ + private static final byte STRING_TYPE = 0x20; + + /* Tag for URL type. */ + private static final byte URL_TYPE = 0x40; + + /* Tag for sequence type. */ + private static final byte SEQUENCE_TYPE = 0x30; + + /* Tag for an alternative type. */ + private static final byte ALTERNATIVE_TYPE = 0x38; + + /* Tag that identifies that size of data size field is 2 bytes. */ + private static final byte SHORT_SIZE = 0x05; + + /* Tag that identifies that size of data size field is 4 bytes. */ + private static final byte NORMAL_SIZE = 0x06; + + /* Tag that identifies that size of data size field is 8 bytes. */ + private static final byte LONG_SIZE = 0x07; + + /* Destination buffer which collects binary data of a data element. */ + protected byte[] writeBuffer = null; + + /* Source buffer which contains binary data of a data element. */ + protected byte[] readBuffer = null; + + /* Current position at the destination buffer. */ + protected long writePos = 0; + + /* Current position at the source buffer. */ + protected long readPos = 0; + + /* Allows to store and retrieve positions at the source buffer. */ + private Stack readPosStack = new Stack(); + + /* + * Constructs the serializer object. + */ + public DataElementSerializer() { + } + + /* + * Serializes given DataElement object, i.e. creates an array of bytes + * representing DataElement as described in Bluetooth Specification + * Version 1.2, vol 3, page 127. + * + * @param data the data element to serialize + * @return an array containing the serialized data element + * @throws IOException if an I/O error occurs + */ + public synchronized byte[] serialize(DataElement data) throws IOException { + writeBuffer = new byte[(int)getDataSize(data)]; + writePos = 0; + writeDataElement(data); + byte[] result = writeBuffer; + writeBuffer = null; + return result; + } + + /* + * Constructs DataElement from byte array containing the element in + * serialized form. + * + * @param data byte array containing the element in serialized form + * @return DataElement constructed from the binary data + * @throws IOException if an I/O error occurs + */ + public synchronized DataElement restore(byte[] data) throws IOException { + readBuffer = data; + readPos = 0; + DataElement result = readDataElement(); + readBuffer = null; + return result; + } + + /* + * Returns the size of DataElement with service information + * to get the total size required to work with this DataElement. + * + * @param data the data element to get packet size for + * @return number of bytes needed to store given data element + */ + public long getDataSize(DataElement data) { + int type = data.getDataType(); + long size = getPureDataSize(data); + if ((type == DataElement.NULL) || (type == DataElement.BOOL) + || (type == DataElement.INT_1) || (type == DataElement.U_INT_1) + || (type == DataElement.INT_2) || (type == DataElement.U_INT_2) + || (type == DataElement.INT_4) || (type == DataElement.U_INT_4) + || (type == DataElement.INT_8) || (type == DataElement.U_INT_8) + || (type == DataElement.INT_16) + || (type == DataElement.U_INT_16) + || (type == DataElement.UUID)) { + return size + 1; + } else if ((type == DataElement.DATSEQ) + || (type == DataElement.DATALT) || (type == DataElement.STRING) + || (type == DataElement.URL)) { + if (size <= 0xffL) { + return size + 2; + } else if (size <= 0xffffL) { + return size + 3; + } else if (size <= 0xffffffffL) { + return size + 5; + } else { + throw new RuntimeException("Data size is too large."); + } + } else { + throw new RuntimeException("Unexpected data type."); + } + } + + /* + * Returns the size of DataElement without service information. + * + * @param data the data element to get pure data size for + * @return pure data size in bytes + */ + public long getPureDataSize(DataElement data) { + switch (data.getDataType()) { + case DataElement.NULL: + return 0; + case DataElement.BOOL: + case DataElement.INT_1: + case DataElement.U_INT_1: + return 1; + case DataElement.INT_2: + case DataElement.U_INT_2: + return 2; + case DataElement.INT_4: + case DataElement.U_INT_4: + return 4; + case DataElement.INT_8: + case DataElement.U_INT_8: + return 8; + case DataElement.INT_16: + case DataElement.U_INT_16: + return 16; + case DataElement.DATSEQ: + case DataElement.DATALT: + long size = 0; + Enumeration elements = (Enumeration)data.getValue(); + while (elements.hasMoreElements()) { + size += getDataSize((DataElement)elements.nextElement()); + } + return size; + case DataElement.STRING: + case DataElement.URL: + return ((String)data.getValue()).length(); + case DataElement.UUID: + return 16; + default: + throw new RuntimeException("Unknown data type."); + } + } + + /* + * Writes given data element into the write buffer. + * + * @param data the data element to write + * @throws IOException if an I/O error occurs + */ + public void writeDataElement(DataElement data) throws IOException { + long size = getPureDataSize(data); + int type = data.getDataType(); + byte typeBits = 0x00; + if ((type == DataElement.NULL) || (type == DataElement.BOOL) + || (type == DataElement.INT_1) || (type == DataElement.U_INT_1) + || (type == DataElement.INT_2) || (type == DataElement.U_INT_2) + || (type == DataElement.INT_4) || (type == DataElement.U_INT_4) + || (type == DataElement.INT_8) || (type == DataElement.U_INT_8) + || (type == DataElement.INT_16) + || (type == DataElement.U_INT_16)) { + switch (type) { + case DataElement.NULL: + writeByte(NULL_DATA); + break; + case DataElement.BOOL: + writeByte(BOOLEAN_DATA); + writeBoolean(data.getBoolean()); + break; + case DataElement.INT_1: + writeByte(INT1_SIGNED); + writeByte((byte)data.getLong()); + break; + case DataElement.U_INT_1: + writeByte(INT1_UNSIGNED); + writeByte((byte)data.getLong()); + break; + case DataElement.INT_2: + writeByte(INT2_SIGNED); + writeShort((short)data.getLong()); + break; + case DataElement.U_INT_2: + writeByte(INT2_UNSIGNED); + writeShort((short)data.getLong()); + break; + case DataElement.INT_4: + writeByte(INT4_SIGNED); + writeInteger((int)data.getLong()); + break; + case DataElement.U_INT_4: + writeByte(INT4_UNSIGNED); + writeInteger((int)data.getLong()); + break; + case DataElement.INT_8: + writeByte(INT8_SIGNED); + writeLong(data.getLong()); + break; + case DataElement.U_INT_8: + writeByte(INT8_UNSIGNED); + writeBytes((byte[])data.getValue()); + break; + case DataElement.INT_16: + writeByte(INT16_SIGNED); + writeBytes((byte[])data.getValue()); + break; + case DataElement.U_INT_16: + writeByte(INT16_UNSIGNED); + writeBytes((byte[])data.getValue()); + break; + } + } else if ((type == DataElement.DATSEQ) + || (type == DataElement.DATALT) || (type == DataElement.STRING) + || (type == DataElement.URL)) { + switch (type) { + case DataElement.DATSEQ: + typeBits = (TYPE_MASK & SEQUENCE_TYPE); + break; + case DataElement.DATALT: + typeBits = (TYPE_MASK & ALTERNATIVE_TYPE); + break; + case DataElement.STRING: + typeBits = (TYPE_MASK & STRING_TYPE); + break; + case DataElement.URL: + typeBits = (TYPE_MASK & URL_TYPE); + break; + } + if (size <= 0xff) { + writeByte(typeBits | (SIZE_MASK & SHORT_SIZE)); + writeByte((byte)size); + } else if (size <= 0xffff) { + writeByte(typeBits | (SIZE_MASK & NORMAL_SIZE)); + writeShort((short)size); + } else { + writeByte(typeBits | (SIZE_MASK & LONG_SIZE)); + writeInteger((int)size); + } + if ((type == DataElement.DATSEQ) || (type == DataElement.DATALT)) { + Enumeration elements = (Enumeration) data.getValue(); + while (elements.hasMoreElements()) { + writeDataElement((DataElement)elements.nextElement()); + } + } else { + writeBytes(((String)data.getValue()).getBytes()); + } + } else if (type == DataElement.UUID) { + writeByte(UUID_16); + String uuid = ((UUID)data.getValue()).toString(); + while (uuid.length() < 32) { + uuid = '0' + uuid; + } + for (int i = 0; i < 16; i++) { + writeByte(Integer.parseInt( + uuid.substring(i * 2, i * 2 + 2), 16)); + } + } else { + throw new RuntimeException("Unknown data type."); + } + } + + /* + * Creates a data element from the binary data in the read buffer. + * + * @return DataElement read + * @throws IOException if an I/O error occurs + */ + public DataElement readDataElement() throws IOException { + byte header = readByte(); + if ((header == NULL_DATA) || (header == BOOLEAN_DATA) + || (header == INT1_SIGNED) || (header == INT1_UNSIGNED) + || (header == INT2_SIGNED) || (header == INT2_UNSIGNED) + || (header == INT4_SIGNED) || (header == INT4_UNSIGNED) + || (header == INT8_SIGNED) || (header == INT8_UNSIGNED) + || (header == INT16_SIGNED) || (header == INT16_UNSIGNED)) { + switch (header) { + case NULL_DATA: + return new DataElement(DataElement.NULL); + case BOOLEAN_DATA: + return new DataElement(readBoolean()); + case INT1_SIGNED: + return new DataElement(DataElement.INT_1, readByte()); + case INT1_UNSIGNED: + return new DataElement(DataElement.U_INT_1, + (readByte() & 0xffL)); + case INT2_SIGNED: + return new DataElement(DataElement.INT_2, readShort()); + case INT2_UNSIGNED: + return new DataElement(DataElement.U_INT_2, + (readShort() & 0xffffL)); + case INT4_SIGNED: + return new DataElement(DataElement.INT_4, readInteger()); + case INT4_UNSIGNED: + return new DataElement(DataElement.U_INT_4, + (readInteger() & 0xffffffffL)); + case INT8_SIGNED: + return new DataElement(DataElement.INT_8, readLong()); + case INT8_UNSIGNED: + return new DataElement(DataElement.U_INT_8, readBytes(8)); + case INT16_SIGNED: + return new DataElement(DataElement.INT_16, readBytes(16)); + case INT16_UNSIGNED: + return new DataElement(DataElement.U_INT_16, readBytes(16)); + } + } else if (((header & TYPE_MASK) == STRING_TYPE) + || ((header & TYPE_MASK) == URL_TYPE) + || ((header & TYPE_MASK) == SEQUENCE_TYPE) + || ((header & TYPE_MASK) == ALTERNATIVE_TYPE)) { + long size = 0; + if ((header & SIZE_MASK) == SHORT_SIZE) { + size = readByte() & 0xffL; + } else if ((header & SIZE_MASK) == NORMAL_SIZE) { + size = readShort() & 0xffffL; + } else if ((header & SIZE_MASK) == LONG_SIZE) { + size = readInteger() & 0xffffffffL; + } else { + System.err.println("Unknown size mask."); + } + if ((header & TYPE_MASK) == STRING_TYPE) { + return new DataElement(DataElement.STRING, + new String(readBytes((int)size))); + } else if ((header & TYPE_MASK) == URL_TYPE) { + return new DataElement(DataElement.URL, + new String(readBytes((int)size))); + } else { + DataElement data = null; + DataElement dataElement = null; + long dataPos = 0; + if ((header & TYPE_MASK) == SEQUENCE_TYPE) { + data = new DataElement(DataElement.DATSEQ); + } else { + data = new DataElement(DataElement.DATALT); + } + while (dataPos < size) { + pushReadPos(); + dataElement = readDataElement(); + dataPos += readPos - popReadPos(); + data.addElement(dataElement); + } + return data; + } + } else if (header == UUID_2) { + return new DataElement(DataElement.UUID, readUUID(2)); + } else if (header == UUID_4) { + return new DataElement(DataElement.UUID, readUUID(4)); + } else if (header == UUID_16) { + return new DataElement(DataElement.UUID, readUUID(16)); + } else { + throw new RuntimeException("Unknown data type."); + } + return null; + } + + /* + * Writes boolean data to the buffer. + * Writes only value given itself. Note that boolean data header + * should be written before. + * + * @param data boolean value to write. + * @throws IOException if an I/O error occurs + */ + public void writeBoolean(boolean data) throws IOException { + writeByte(data ? 1 : 0); + } + + /* + * Writes 1-byte data to the buffer. + * + * @param data byte value to write. + * @throws IOException if an I/O error occurs + */ + public void writeByte(long data) throws IOException { + if (writePos < 0 || writePos >= writeBuffer.length) { + throw new IndexOutOfBoundsException(); + } + writeBuffer[(int)writePos++] = (byte)data; + } + + /* + * Writes 2-byte data to the buffer. + * + * @param data 2-byte value to write. + * @throws IOException if an I/O error occurs + */ + public void writeShort(short data) throws IOException { + writeByte((byte)((data >>> 8) & 0xff)); + writeByte((byte)((data >>> 0) & 0xff)); + } + + /* + * Writes 4-byte data to the connection. + * + * @param data 4-byte value to write. + * @throws IOException if an I/O error occurs + */ + public void writeInteger(int data) throws IOException { + writeShort((short)((data >>> 16) & 0xffff)); + writeShort((short)((data >>> 0) & 0xffff)); + } + + /* + * Writes 8-byte data to the connection. + * + * @param data 8-byte value to write. + * @throws IOException if an I/O error occurs + */ + public void writeLong(long data) throws IOException { + writeInteger((int)((data >>> 32) & 0xffffffff)); + writeInteger((int)((data >>> 0) & 0xffffffff)); + } + + /* + * Writes given data to the connection. + * + * @param data bytes to write. + * @throws IOException if an I/O error occurs + */ + public void writeBytes(byte[] data) throws IOException { + if (writePos < 0 || writePos + data.length > writeBuffer.length) { + throw new IndexOutOfBoundsException(); + } + System.arraycopy(data, 0, writeBuffer, (int)writePos, data.length); + writePos += data.length; + } + + /* + * Reads boolean value from the connection. + * + * @return boolean value recieved. + * @throws IOException if an I/O error occurs + */ + public boolean readBoolean() throws IOException { + return (readByte() != 0); + } + + /* + * Reads 1-byte value from the connection. + * + * @return byte recieved. + * @throws IOException if an I/O error occurs + */ + public byte readByte() throws IOException { + if (readPos < 0 || readPos >= readBuffer.length) { + throw new IndexOutOfBoundsException(); + } + return readBuffer[(int)readPos++]; + } + + /* + * Reads 2-byte value from the connection. + * + * @return short which is the 2 bytes read. + * @throws IOException if an I/O error occurs + */ + public short readShort() throws IOException { + int data1 = ((int)readByte()) & 0xff; + int data2 = ((int)readByte()) & 0xff; + return (short)((data1 << 8) + (data2 << 0)); + } + + /* + * Reads 4-byte value from the connection. + * + * @return int which is the 4 bytes read. + * @throws IOException if an I/O error occurs + */ + public int readInteger() throws IOException { + int data1 = ((int)readShort()) & 0xffff; + int data2 = ((int)readShort()) & 0xffff; + return ((data1 << 16) + (data2 << 0)); + } + + /* + * Reads 8-byte value from the connection. + * + * @return long which is the 8 bytes read. + * @throws IOException if an I/O error occurs + */ + public long readLong() throws IOException { + long data1 = ((long)readInteger()) & 0xffffffffL; + long data2 = ((long)readInteger()) & 0xffffffffL; + return ((data1 << 32) + (data2 << 0)); + } + + /* + * Reads given number of bytes from the connection. + * + * @param size number of bytes to read. + * @return array of bytes read. + * @throws IOException if an I/O error occurs + */ + public byte[] readBytes(int size) throws IOException { + byte[] data = new byte[size]; + int dataPos = 0; + if (readPos < 0 || readPos + data.length > readBuffer.length) { + throw new IndexOutOfBoundsException(); + } + System.arraycopy(readBuffer, (int)readPos, data, 0, data.length); + readPos += data.length; + return data; + } + + /* + * Reads UUID of a given size. + * + * @param len number of bytes to read + * @return UUID created from len bytes + * @throws IOException if an I/O error occurs + */ + public UUID readUUID(int len) throws IOException { + String uuid = ""; + for (int i = 0; i < len; i++) { + String digit = Integer.toHexString(readByte() & 0xff); + if (digit.length() == 1) digit = '0' + digit; + uuid += digit; + } + return new UUID(uuid, len < 16); + } + + /* Saves the current read position. */ + private void pushReadPos() { + readPosStack.push(new Long(readPos)); + } + + /* Extracts saved read position. */ + private long popReadPos() { + return ((Long)readPosStack.pop()).longValue(); + } + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/DataL2CAPReaderWriter.java b/java/midp/com/sun/jsr082/bluetooth/DataL2CAPReaderWriter.java new file mode 100644 index 000000000..a984eed35 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/DataL2CAPReaderWriter.java @@ -0,0 +1,172 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Stack; +import javax.bluetooth.DataElement; +import javax.bluetooth.L2CAPConnection; +import javax.bluetooth.UUID; + +/* + * Logical Link Control and Adaptation Protocol connection reader/writer. + */ +public class DataL2CAPReaderWriter extends DataElementSerializer { + + /* The L2CAP connection to read from or write to. */ + private L2CAPConnection con = null; + + /* Actual size of the read buffer. */ + private long readSize = 0; + + /* + * Constructs reader-and-writer object for the given connection. + * + * @param con the L2CAP connection to create reader-writer for. + */ + public DataL2CAPReaderWriter(L2CAPConnection con) throws IOException { + this.con = con; + writeBuffer = new byte[con.getTransmitMTU()]; + readBuffer = new byte[con.getReceiveMTU()]; + } + + /* + * Writes 1-byte data to the connection. + * + * @param data byte value to write. + */ + public void writeByte(long data) throws IOException { + if ((writePos < 0) || (writePos >= writeBuffer.length)) { + throw new IndexOutOfBoundsException(); + } + writeBuffer[(int)writePos] = (byte)data; + writePos++; + if (writePos == writeBuffer.length) { + con.send(writeBuffer); + writePos = 0; + } + } + + /* + * Writes given data to the connection. + * + * @param data bytes to write. + */ + public void writeBytes(byte[] data) throws IOException { + if ((writePos < 0) || (writePos >= writeBuffer.length)) { + throw new IndexOutOfBoundsException(); + } + int dataPos = 0; + + while ((data.length - dataPos) >= ((writeBuffer.length - writePos))) { + int length = writeBuffer.length - ((int) writePos); + System.arraycopy(data, (int) dataPos, writeBuffer, (int) writePos, + (int) length); + con.send(writeBuffer); + writePos = 0; + dataPos += length; + } + int length = data.length - dataPos; + + if (length > 0) { + System.arraycopy(data, (int) dataPos, writeBuffer, (int) writePos, + (int) length); + } + writePos += length; + } + + /* + * Flushes write buffer to the conection. + */ + public void flush() throws IOException { + if ((writePos < 0) || (writePos >= writeBuffer.length)) { + throw new IndexOutOfBoundsException(); + } + if (writePos == 0) { + return; + } + byte[] tempBuffer = new byte[(int)writePos]; + System.arraycopy(writeBuffer, 0, tempBuffer, 0, (int)writePos); + con.send(tempBuffer); + writePos = 0; + } + + /* + * Reads 1-byte value from the connection. + * + * @return byte recieved. + */ + public byte readByte() throws IOException { + if ((readPos == 0) || (readPos == readSize)) { + if ((readSize = con.receive(readBuffer)) == 0) { + throw new IOException("Empty packet is received"); + } + readPos = 0; + } else if ((readPos < 0) || (readPos > readSize)) { + throw new IndexOutOfBoundsException(); + } + + if (readSize == -1) { + throw new IOException("Reached end of stream"); + } + + byte data = readBuffer[(int)readPos]; + readPos++; + return data; + } + + /* + * Reads given number of bytes from the connection. + * + * @param size number of bytes to read. + * @return array of bytes read. + */ + public byte[] readBytes(int size) throws IOException { + byte[] data = new byte[size]; + int dataPos = 0; + + if ((readPos == 0) || (readPos == readSize)) { + readSize = con.receive(readBuffer); + readPos = 0; + } else if ((readPos < 0) || (readPos > readSize)) { + throw new IndexOutOfBoundsException(); + } + + while ((data.length - dataPos) > (readSize - readPos)) { + int length = (int) (readSize - readPos); + System.arraycopy(readBuffer, (int)readPos, data, (int)dataPos, + length); + dataPos += length; + readSize = con.receive(readBuffer); + readPos = 0; + } + int length = data.length - dataPos; + System.arraycopy(readBuffer, (int)readPos, data, (int)dataPos, + length); + readPos += length; + return data; + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/DiscoveryAgentImpl.java b/java/midp/com/sun/jsr082/bluetooth/DiscoveryAgentImpl.java new file mode 100644 index 000000000..4ee913970 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/DiscoveryAgentImpl.java @@ -0,0 +1,336 @@ +/* + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import javax.bluetooth.BluetoothStateException; +import javax.bluetooth.DiscoveryAgent; +import javax.bluetooth.DiscoveryListener; +import javax.bluetooth.RemoteDevice; +import javax.bluetooth.UUID; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; + +/* + * The DiscoveryAgentImpl class is a DiscoveryAgent + * API class implementation which does not extend this API class. + */ +public final class DiscoveryAgentImpl { + /* Calls inquiry completion callback in a separate thread. */ + class Completed implements Runnable { + /* type of completion. */ + private int discType; + /* listener to be called. */ + private DiscoveryListener listener; + /* Constructs an instance and starts the the thread. */ + Completed(DiscoveryListener listener, int discType) { + this.listener = listener; + this.discType = discType; + new Thread(this).start(); + } + /* Implements Runnable. */ + public void run() { + if (listener != null) { + listener.inquiryCompleted(discType); + } + } + } + + /* Set to false in RR version - then the javac skip the code. */ + private static final boolean DEBUG = false; + + /* + * maximum number of allowed UUIDS in search uuids sequence + */ + private static final int MAX_ALLOWED_UUIDS = 12; + + /* + * Keeps an instance to the object of this class to be + * accessible from the implementation. + */ + private static DiscoveryAgentImpl instance; + + /* Keeps the RemoteDeviceImpl references of known devices. */ + private Hashtable knownDevices = new Hashtable(); + + /* Keeps the RemoteDeviceImpl references of cached devices. */ + private Hashtable cachedDevices = new Hashtable(); + + /* + * Keeps the listener of the device discovery inquire. + * Also, it is used as flag that a device is in inquire mode. + */ + private DiscoveryListener d_listener; + + /* Keeps the lock object for device discovery synchronization. */ + private Object d_lock = new Object(); + + /* Keeps the reference to module responsible for selecting services. */ + private SelectServiceHandler selectServiceHandler = + new SelectServiceHandler(this); + + /* Constructs the single instance. */ + private DiscoveryAgentImpl() {} + + public RemoteDevice[] retrieveDevices(int option) { + switch (option) { + case DiscoveryAgent.CACHED: + // IMPL_NOTE: use native cache keeping addresses of found devices + // to share the cache between multiple isolates + return getCachedDevices(); + case DiscoveryAgent.PREKNOWN: + Vector pk = BCC.getInstance().getPreknownDevices(); + if (pk == null || pk.size() == 0) { + return null; + } + RemoteDevice[] res = new RemoteDevice[pk.size()]; + for (int i = 0; i < pk.size(); i++) { + String addr = (String)pk.elementAt(i); + res[i] = getRemoteDevice(addr); + } + return res; + default: + throw new IllegalArgumentException("Invalid option value: " + + option); + } + } + + + /* + * Adds address of remote device found during inquiry request to internal + * inquiry cache. + * + * The method does nothing if the RemoteDevice is already in the cache. + */ + public void addCachedDevice(String addr) { + RemoteDevice rd = getRemoteDevice(addr); + synchronized (cachedDevices) { + cachedDevices.put(addr, rd); + } + } + + // JAVADOC COMMENT ELIDED + private RemoteDevice[] getCachedDevices() { + synchronized (cachedDevices) { + int len = cachedDevices.size(); + if (len == 0) { + return null; + } + RemoteDevice[] res = new RemoteDevice[len]; + Enumeration e = cachedDevices.elements(); + for (int i = 0; e.hasMoreElements(); i++) { + res[i] = (RemoteDevice)e.nextElement(); + } + return res; + } + } + + public boolean startInquiry(int accessCode, DiscoveryListener listener) + throws BluetoothStateException { + + if (accessCode != DiscoveryAgent.GIAC && + accessCode != DiscoveryAgent.LIAC && + (accessCode < 0x9E8B00 || accessCode > 0x9E8B3F)) { + throw new IllegalArgumentException("Access code is out of range: " + + accessCode); + } + + if (listener == null) { + throw new NullPointerException("null listener"); + } + + /* IMPL_NOTE see + // kvem/classes/com/sun/kvem/jsr082/impl/bluetooth/ + // BTDeviceDiscoverer.java + // heck what access codes should be supported. + // Return false if access code is not supported. + */ + + synchronized (d_lock) { + if (d_listener != null) { + throw new BluetoothStateException( + "The previous device discovery is running..."); + } + d_listener = listener; + + /* process the inquiry in the device specific way */ + return startInquiry(accessCode); + } + } + + private boolean startInquiry(int accessCode) throws BluetoothStateException { + return BluetoothStack.getEnabledInstance().startInquiry( + accessCode, d_listener); + } + + public boolean cancelInquiry(DiscoveryListener listener) { + if (listener == null) { + throw new NullPointerException("null listener"); + } + synchronized (d_lock) { + + /* no inquiry was started */ + if (d_listener == null) { + return false; + } + + /* not valid listener */ + if (d_listener != listener) { + return false; + } + + /* process the inquiry in the device specific way */ + cancelInquiry(); + } + + inquiryCompleted(DiscoveryListener.INQUIRY_TERMINATED); + return true; + } + + /* + * Cancels inquiry in device specific way. + */ + private void cancelInquiry() { + BluetoothStack.getInstance().cancelInquiry(d_listener); + } + + /* + * Porting interface: this method is used by the device specific + * implementation to notify this class, that the current inquire + * has been completed. + * + * @param discType type of completion: + * DiscoveryListener.INQUIRY_COMPLETED, or + * DiscoveryListener.INQUIRY_TERMINATED, or + * DiscoveryListener.INQUIRY_ERROR + */ + public void inquiryCompleted(int discType) { + DiscoveryListener listener; + synchronized (d_lock) { + listener = d_listener; + d_listener = null; + } + + new Completed(listener, discType); + } + + /* + * Porting interface: this method is used by the device specific + * implementation to create the RemoteDevice object by address. + * + * Also, this method puts the new remote devices into cache of + * known devices. + * + * @param addr address of remote device to be created + * + * @return new RemoteDeviceImplinstance if device with address + * given is unknown, the known one otherwise. + */ + public RemoteDeviceImpl getRemoteDevice(String addr) { + synchronized (knownDevices) { + addr = addr.toUpperCase(); + RemoteDeviceImpl rd = (RemoteDeviceImpl) knownDevices.get(addr); + + if (rd == null) { + rd = new RemoteDeviceImpl(addr); + knownDevices.put(addr, rd); + } + return rd; + } + } + + public int searchServices(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev, + DiscoveryListener discListener) throws BluetoothStateException { + + if (DEBUG) { + System.out.println("searchServices: "); + System.out.println("\tattrSet=" + attrSet); + + if (attrSet != null) { + for (int i = 0; i < attrSet.length; i++) { + System.out.println("\tattrSet[" + i + "]=0x" + attrSet[i]); + } + } + System.out.println("\tuuidSet=" + uuidSet); + + if (uuidSet != null) { + for (int i = 0; i < uuidSet.length; i++) { + System.out.println("\tuuidSet[" + i + "]=" + uuidSet[i]); + } + } + System.out.println("\tadderess=" + btDev.getBluetoothAddress()); + } + if (uuidSet == null) { + throw new NullPointerException("UUID set is null"); + } + + if (uuidSet.length == 0 || uuidSet.length > MAX_ALLOWED_UUIDS ) { + throw new IllegalArgumentException("Invalid UUID set length"); + } + + if (btDev == null) { + throw new NullPointerException("null instance of RemoteDevice"); + } + + /* the 'transID' is assigned by service discoverer */ + int transID = ServiceDiscovererFactory.getServiceDiscoverer(). + searchService( + ServiceSearcherBase.extendByStandardAttrs(attrSet), + ServiceSearcherBase.removeDuplicatedUuids(uuidSet), + btDev, discListener); + + if (DEBUG) { + System.out.println("\ttransID=" + transID); + } + return transID; + } + + public boolean cancelServiceSearch(int transID) { + if (DEBUG) { + System.out.println("cancelServiceSearch: transID=" + transID); + } + return ServiceDiscovererFactory.getServiceDiscoverer().cancel(transID); + } + + public String selectService(UUID uuid, int security, boolean master) + throws BluetoothStateException { + + // use the separated class to light this one + return selectServiceHandler.selectService(uuid, security, master); +// return ServiceDiscovererFactory.getServiceDiscoverer(). +// selectService(uuid, security, master, this); + } + + /* + * Returns the instance of this singleton constructing it if needed. + * @return the only instance of DiscoveryAgentImpl. + */ + public static synchronized DiscoveryAgentImpl getInstance() { + if (instance == null) { + instance = new DiscoveryAgentImpl(); + } + + return instance; + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/EncryptionChangeEvent.java b/java/midp/com/sun/jsr082/bluetooth/EncryptionChangeEvent.java new file mode 100644 index 000000000..7f846d82b --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/EncryptionChangeEvent.java @@ -0,0 +1,63 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; + +/* + * Encryption change event. + */ +public class EncryptionChangeEvent extends BluetoothEvent { + + /* ACL connection handle for which encryption change has been performed. */ + private int handle; + + /* Indicates whether the change has occured. */ + private boolean success; + + /* Indicates whether the link encryption is enabled. */ + private boolean enabled; + + /* + * Class constructor. + * + * @param aclHandle ACL connection handle + * @param succ true if the change occured, false otherwise + * @param on true if the change occured, false otherwise + */ + public EncryptionChangeEvent(int aclHandle, boolean succ, boolean on) { + handle = aclHandle; + success = succ; + enabled = on; + } + + /* + * Processes this event. + */ + public void process() { + BluetoothStack.getInstance().onEncryptionChange(handle, success); + } + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/InquiryCompleteEvent.java b/java/midp/com/sun/jsr082/bluetooth/InquiryCompleteEvent.java new file mode 100644 index 000000000..a9ee53438 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/InquiryCompleteEvent.java @@ -0,0 +1,54 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; + +/* + * Inquiry completion event. + */ +public class InquiryCompleteEvent extends BluetoothEvent { + + /* Indicates whether the inquiry succeeded. */ + private boolean success; + + /* + * Class constructor. + * + * @param succ true if the inquiry succeeded, false otherwise + */ + public InquiryCompleteEvent(boolean succ) { + super.eventName = "InquiryCompleteEvent"; + success = succ; + } + + /* + * Processes this event. + */ + public void process() { + BluetoothStack.getInstance().onInquiryComplete(success); + } + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/InquiryResult.java b/java/midp/com/sun/jsr082/bluetooth/InquiryResult.java new file mode 100644 index 000000000..83a39fdde --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/InquiryResult.java @@ -0,0 +1,71 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; + +import javax.bluetooth.DeviceClass; + +/* +* Inquiry result record. +*/ +class InquiryResult { + + /* Bluetooth address of a discovered device. */ + private String address; + + /* Class of a discovered device. */ + private DeviceClass deviceClass; + + /* + * Class constructor. + * + * @param addr Bluetooth address of a discovered device + * @param cod class of a discovered device + */ + public InquiryResult(String addr, int cod) { + address = addr; + deviceClass = new DeviceClass(cod); + } + + /* + * Returns Bluetooth address of the remote device. + * + * @return Bluetooth address of the remote device + */ + public String getAddress() { + return address; + } + + /* + * Returns class of the remote device. + * + * @return class of the device + */ + public DeviceClass getDeviceClass() { + return deviceClass; + } + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/InquiryResultEvent.java b/java/midp/com/sun/jsr082/bluetooth/InquiryResultEvent.java new file mode 100644 index 000000000..174bf5b25 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/InquiryResultEvent.java @@ -0,0 +1,67 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; + +/* + * Inquiry result event. + */ +public class InquiryResultEvent extends BluetoothEvent { + + /* An array of inquiry results. */ + private InquiryResult[] results; + + /* + * Creates event using a single inquiry result record. + * + * @param result inquiry result record + */ + public InquiryResultEvent(InquiryResult result) { + super.eventName = "InquiryResultEvent"; + results = new InquiryResult[1]; + results[0] = result; + } + + /* + * Creates event using an array of results. + * + * @param resultsArray an array of result records + */ + public InquiryResultEvent(InquiryResult[] resultsArray) { + results = resultsArray; + } + + /* + * Processes this event. + */ + public void process() { + BluetoothStack stack = BluetoothStack.getInstance(); + for (int i = 0; i < results.length; i++) { + stack.onInquiryResult(results[i]); + } + } + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/JavaSDPClient.java b/java/midp/com/sun/jsr082/bluetooth/JavaSDPClient.java new file mode 100644 index 000000000..f0648459d --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/JavaSDPClient.java @@ -0,0 +1,170 @@ +/* + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; + +import javax.bluetooth.UUID; + +/* + * JavaSDPClient class provides a client side of SDP connection as described in + * Bluetooth Specification version 1.2. + */ +public class JavaSDPClient implements SDPClient{ + + /* Transport connection for this client. */ + private SDPClientConnection connection = null; + + /* Bluetooth address this client is connected to. */ + private String address = null; + + private Hashtable ssTrnas = new Hashtable(); + + private static final boolean DEBUG = false; + + /* + * Constructs an JavaSDPClient object and opens SDP connection + * to the remote device with the specified Bluetooth address. + * + * @param bluetoothAddress bluetooth address of SDP server + */ + public JavaSDPClient(String bluetoothAddress) throws IOException { + address = bluetoothAddress; + try { + connection = SDPClientConnection.getSDPClientConnection(address); + } catch (IOException e) { + //TODO add proper handling routines + e.printStackTrace(); + } + } + + /* + * Closes connection of this client to the specified server. + * + * @throws IOException if no connection is open + */ + public void close() throws IOException { + if (DEBUG) { + System.out.println("* JavaSDPClient: finishing transactions"); + } + Enumeration trs = (Enumeration) ssTrnas.keys(); + synchronized (ssTrnas) { + while (trs.hasMoreElements()) { + Object key = trs.nextElement(); + SDPClientTransaction tr = (SDPClientTransaction) ssTrnas + .get(key); + if (tr != null) { + tr.finish(); + } + } + ssTrnas.clear(); + } + if (DEBUG) { + System.out.println("* JavaSDPClient: closing connection"); + } + if (connection != null) { + connection.release(); + connection = null; + } + if (DEBUG) { + System.out.println("* JavaSDPClient: Canceling all receivers"); + } + SDPClientReceiver.cancel(); + if (DEBUG) { + System.out.println("* JavaSDPClient: closed"); + } + } + + public void removeTransaction( String transID ) { + synchronized (ssTrnas) { + ssTrnas.remove( transID ); + } + } + + public SDPClientConnection getConnection() { + return connection; + } + + /* + * Initiates ServiceSearch transaction that is used to search for + * services that have all the UUIDs specified on a server. + */ + public void serviceSearchRequest(UUID[] uuidSet, int transactionID, + SDPResponseListener listener) throws IOException { + SDPClientTransaction tr = null; + tr = new ClientServiceSearchTransaction(this, transactionID, listener, uuidSet); + synchronized (ssTrnas) { + ssTrnas.put(tr.getID(), tr); + } + tr.start(); + } + + /* + * Initiates ServiceAttribute transaction that retrieves + * specified attribute values from a specific service record. + */ + public void serviceAttributeRequest(int serviceRecordHandle, int[] attrSet, + int transactionID, SDPResponseListener listener) throws IOException { + SDPClientTransaction tr = new ClientServiceAttributeTransaction(this, transactionID, listener, serviceRecordHandle, attrSet); + synchronized (ssTrnas) { + ssTrnas.put(tr.getID(), tr); + } + tr.start(); + } + + /* + * Initiates ServiceSearchAttribute transaction that searches for services + * on a server by UUIDs specified and retrieves values of specified + * parameters for service records found. + */ + public void serviceSearchAttributeRequest(int[] attrSet, UUID[] uuidSet, + int transactionID, SDPResponseListener listener) throws IOException { + SDPClientTransaction tr = new ClientServiceSearchAttributeTransaction( this, transactionID, listener, attrSet, uuidSet ); + synchronized (ssTrnas) { + ssTrnas.put(tr.getID(), tr); + } + tr.start(); + } + + /* + * Cancels transaction with given ID. + */ + public boolean cancelServiceSearch(int transactionID) { + SDPClientTransaction tr = null; + boolean isCanceled = false; + synchronized (ssTrnas) { + String id = "" + transactionID + "_" + SDPClientTransaction.SDP_SERVICE_SEARCH_REQUEST; + tr = (SDPClientTransaction)ssTrnas.get( id ); + ssTrnas.remove(id); + } + if (tr != null) { + tr.cancel(SDPResponseListener.TERMINATED); + isCanceled = true; + } + return isCanceled; + } + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/JavacallBluetoothStack.jpp b/java/midp/com/sun/jsr082/bluetooth/JavacallBluetoothStack.jpp new file mode 100644 index 000000000..8571ddd5f --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/JavacallBluetoothStack.jpp @@ -0,0 +1,169 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; +import com.sun.jsr082.bluetooth.BluetoothUtils; + +/* + * BluetoothStack implementation which relies on HCI command/event flow. + */ +public class JavacallBluetoothStack extends BluetoothStack { + + /* Maximum HCI packet size as defined by Bluetooth 1.1 specification. */ + private final int MAX_HCI_PACKET_SIZE = 257; + /* This values must be identical with Javanotify events IDs */ + /*in the btStackEvent.c file*/ + /* HCI Inquiry Complete event code. */ + protected final static int JAVACALL_BT_EVENT_INQUIRY_COMPLETE = 1; + /* HCI Inquiry Result event code. */ + protected final static int JAVACALL_BT_EVENT_INQUIRY_RESULT = 2; + /* HCI Authentication Complete event code. */ + protected final static int JAVACALL_BT_EVENT_AUTHENTICATION_COMPLETE = 3; + /* HCI Remote Name Request Complete event code. */ + protected final static int JAVACALL_BT_EVENT_REMOTE_NAME_COMPLETE = 4; + /* HCI Encrypt Change event code. */ + protected final static int JAVACALL_BT_EVENT_ENCRYPTION_CHANGE = 5; + /* HCI Service Discovered event code */ + protected final static int JAVACALL_BT_EVENT_SERVICE_DISCOVERED = 6; + /* HCI Service Search Completed event code */ + protected final static int JAVACALL_BT_EVENT_SERVICE_SEARCH_COMPLETED = 7; + + /* Internal byte array for storing incoming HCI events. */ + private byte[] buffer = new byte[MAX_HCI_PACKET_SIZE]; + + /* + * Extracts Bluetooth address from the internal buffer. + * + * @param offset offset in the internal buffer + * @return Bluetooth address consisting of 12 hexadecimal characters + */ + protected String extractAddress(int offset) { + byte[] addr = new byte[BluetoothUtils.BTADDR_SIZE]; + System.arraycopy(buffer, offset, addr, 0, BluetoothUtils.BTADDR_SIZE); + return BluetoothUtils.getAddressString(addr); + } + + /* + * Extracts string (e.g. friendly name) from the internal buffer. The + * string is stored in UTF-8 format and is terminated by NULL character. + * + * @param offset offset in the internal buffer + * @return Java string retrieved from binary event data + */ + protected String extractString(int offset) { + int length = 0; + while (offset + length < MAX_HCI_PACKET_SIZE && + buffer[offset + length] != 0) { + length++; + } + // IMPL_NOTE: UTF-8 encoding support for Java strings? +// return new String(buffer, offset, length, "UTF-8"); + return stringUTF8(buffer, offset, length); + } + + /* + * Creates Java String object from UTF-8 encoded string. + * + * @param buffer buffer containing the string in UTF-8 format + * @param offset offset of the first character in the buffer + * @param length length of the encoded string in bytes + * @return Java String object containing the string in UTF-16 format + */ + protected static native String stringUTF8(byte[] buffer, int offset, + int length); + + /* + * Extracts 2 octets from the internal buffer. + * + * @param offset offset in the internal buffer + * @return 16 bits of data from the given offset + */ + protected int extractShort(int offset) { + return ((int)buffer[offset] & 0xff) | + (((int)buffer[offset + 1] & 0xff) << 8); + } + + /* + * Retrieves Bluetooth event from HCI event data containing in the + * internal buffer. + * + * @return BluetoothEvent subclass instance + */ +// protected BluetoothEvent retrieveEvent(Object handle) { + protected BluetoothEvent retrieveEvent(Object handle) { + readData(buffer); + int code = buffer[0]; + int len = buffer[1]; + switch (code) { + case JAVACALL_BT_EVENT_INQUIRY_COMPLETE: + return new InquiryCompleteEvent(buffer[2] == 0); + case JAVACALL_BT_EVENT_INQUIRY_RESULT: { + int num = buffer[2]; + int offset = 3; + InquiryResult[] results = new InquiryResult[num]; + for (int i = 0; i < num; i++) { + String addr = extractAddress(offset); + int cod = 0; + for (int j = 0; j < 3; j++) { + cod |= ((int)buffer[offset + 9 + j] & 0xff) << + (16 - j * 8); + } + results[i] = new InquiryResult(addr, cod); + offset += 14; // 14 is the size of the response structure + } + return new InquiryResultEvent(results); + } + case JAVACALL_BT_EVENT_AUTHENTICATION_COMPLETE: + return new AuthenticationCompleteEvent(extractShort(3), + buffer[2] == 0); + case JAVACALL_BT_EVENT_REMOTE_NAME_COMPLETE: + return new NameResultEvent(extractAddress(3), extractString(9)); + case JAVACALL_BT_EVENT_ENCRYPTION_CHANGE: + return new EncryptionChangeEvent(extractShort(3), + buffer[2] == 0, buffer[5] == 1); +// #ifdef USE_NATIVE_SDDB + case JAVACALL_BT_EVENT_SERVICE_DISCOVERED: { + int transID, recHandle; + int offset = 2; + transID = extractShort(offset) + (extractShort(offset+2) << 16); + offset = offset + 4; + recHandle = extractShort(offset) + (extractShort(offset+2) << 16); + return new ServiceDiscoveredEvent(transID, recHandle); + } + case JAVACALL_BT_EVENT_SERVICE_SEARCH_COMPLETED: { + int transID; + int offset = 2; + transID = extractShort(offset) + (extractShort(offset+2) << 16); + return new ServiceSearchCompletedEvent(transID, + buffer[offset+4]); + } +// #endif + default: + return null; + } + } + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/LocalDeviceImpl.java b/java/midp/com/sun/jsr082/bluetooth/LocalDeviceImpl.java new file mode 100644 index 000000000..58007db9d --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/LocalDeviceImpl.java @@ -0,0 +1,268 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import com.sun.jsr082.obex.SessionNotifierImpl; +import javax.microedition.io.Connection; +import javax.bluetooth.DeviceClass; +import javax.bluetooth.DiscoveryAgent; +import javax.bluetooth.BluetoothStateException; +import javax.bluetooth.ServiceRecord; +import javax.bluetooth.ServiceRegistrationException; + +/* + * The LocalDeviceImpl class is a LocalDevice + * API class implementation. This is a singleton class. + */ +public final class LocalDeviceImpl { + + /* Set to false in RR version - then the javac skip the code. */ + private static final boolean DEBUG = false; + + /* Keeps this singleton object. */ + private static LocalDeviceImpl instance; + + /* Keeps bluetooth address of this device. */ + private String bluetoothAddress; + + /* Timeout canceller of limited discoverable mode. */ + private CancelerOfLIAC cancelerOfLIAC = new CancelerOfLIAC(); + + /* + * Device should not be in LIAC for more than 1 minute, + * then return to the previous mode. + */ + private class CancelerOfLIAC implements Runnable { + /* One minute. */ + private long MINUTE = 60000; + /* Specifies the delay for timeout checks. */ + private int RETRY_DELAY = 100; // ms + /* Saved access code to get back to at timeout. */ + private int savedCode; + /* Keeps canceller start time to check if timeout expired. */ + private long startTime = -1; + /* Flaggs if LIAC mode has been cancelled from outside. */ + private boolean isCanceledFromOutside = true; + + /* + * Starts timeout killer if new discoverable mode is LIAC. + * + * @param oldCode the previous value of discoverable mode. + * @param newCode discoverable mode that has been set just. + */ + synchronized void notifyNewAccessCode(int oldCode, int newCode) { + if (newCode == oldCode) { + return; + } + savedCode = oldCode; + + if (newCode == DiscoveryAgent.LIAC) { + // the currentCode was not LIAC - start a killer + startTime = System.currentTimeMillis(); + new Thread(this).start(); + } else { + /* + * startTime != -1 if the killer is running, but + * this method may be called by the killer itself - + * then there is no need to stop it. + */ + boolean stopKiller = startTime != -1 && isCanceledFromOutside; + startTime = -1; + isCanceledFromOutside = false; + + if (stopKiller) { + try { + wait(); + } catch (InterruptedException e) {} + } + } + } + + /* + * Implements of run() of Runnable interface. + */ + public void run() { + while (true) { + try { + Thread.sleep(RETRY_DELAY); + } catch (InterruptedException e) {} // ignore + + synchronized (this) { + // the access code was changed by application + if (startTime == -1) { + notify(); + return; + } + // minute is running yet + if (System.currentTimeMillis() - startTime < MINUTE) { + continue; + } + // minute is over - change the mode back + isCanceledFromOutside = false; + boolean res = false; + + try { + res = setDiscoverable(savedCode); + } catch (BluetoothStateException e) {} + + if (!res) { + // not now - h-m-m, ok, try later then + isCanceledFromOutside = true; + continue; + } + return; + } + } + } + } // end of class 'CancelerOfLIAC' definition + + /* + * Constructs the only instance of LocalDeviceImpl. + */ + private LocalDeviceImpl() { + } + + /* + * Retrieves singleton instance. + * + * @return the only instance of LocalDeviceImpl + * @throws BluetoothStateException if an error occured. + */ + public static synchronized LocalDeviceImpl getInstance() + throws BluetoothStateException { + if (instance == null) { + instance = new LocalDeviceImpl(); + } + return instance; + } + + // JAVADOC COMMENT ELIDED + public String getFriendlyName() { + return BCC.getInstance().getFriendlyName(); + } + + // JAVADOC COMMENT ELIDED + public DeviceClass getDeviceClass() { + return BCC.getInstance().getDeviceClass(); + } + + // JAVADOC COMMENT ELIDED + public String getProperty(String property) { + return System.getProperty(property); + } + + // JAVADOC COMMENT ELIDED + public int getDiscoverable() { + return BCC.getInstance().getAccessCode(); + } + + // JAVADOC COMMENT ELIDED + public String getBluetoothAddress() { + return BCC.getInstance().getBluetoothAddress(); + } + + // JAVADOC COMMENT ELIDED + public boolean setDiscoverable(int accessCode) + throws BluetoothStateException { + // Check if the specified mode has a valid value + if (accessCode != DiscoveryAgent.GIAC && + accessCode != DiscoveryAgent.LIAC && + accessCode != DiscoveryAgent.NOT_DISCOVERABLE && + (accessCode < 0x9E8B00 || accessCode > 0x9E8B3F)) { + throw new IllegalArgumentException("Access code is out of range: " + + "0x" + Integer.toHexString(accessCode).toUpperCase()); + } + synchronized (cancelerOfLIAC) { + /* + * Accroding to the spec, the device should only be limited + * discoverable (DiscoveryAgent.LIAC) for 1 minute - + * then back to the PREVIOUS discoverable mode. + */ + int oldAccessCode = BCC.getInstance().getAccessCode(); + if (BCC.getInstance().setAccessCode(accessCode)) { + cancelerOfLIAC.notifyNewAccessCode(oldAccessCode, accessCode); + if (accessCode != DiscoveryAgent.NOT_DISCOVERABLE) { + // Start SDDB if discoverable mode was set successfully + // IMPL_NOTE: Do we really need this step? + SDDB.getInstance(); + } + return true; + } + } + return false; + } + + // JAVADOC COMMENT ELIDED + public ServiceRecord getRecord(Connection notifier) { + if (notifier == null) { + throw new NullPointerException("Null notifier specified."); + } + if (!(notifier instanceof BluetoothNotifier)) { + if (!(notifier instanceof SessionNotifierImpl)) { + throw new IllegalArgumentException("Invalid notifier class."); + } + Connection transport = + ((SessionNotifierImpl)notifier).getTransport(); + if (!(transport instanceof BluetoothNotifier)) { + throw new IllegalArgumentException("Invalid notifier class."); + } + return ((BluetoothNotifier)transport).getServiceRecord(); + } + return ((BluetoothNotifier)notifier).getServiceRecord(); + } + + // JAVADOC COMMENT ELIDED + public void updateRecord(ServiceRecord srvRecord) + throws ServiceRegistrationException { + if (DEBUG) { + System.out.println("LocalDeviceImpl.updateRecord"); + } + if (srvRecord == null) { + throw new NullPointerException("Null record specified."); + } + if (!(srvRecord instanceof ServiceRecordImpl)) { + throw new IllegalArgumentException("Invalid service record class."); + } + ServiceRecordImpl record = (ServiceRecordImpl)srvRecord; + BluetoothNotifier notifier = record.getNotifier(); + if (notifier == null) { + throw new IllegalArgumentException( + "Service record is not from local SDDB."); + } + notifier.updateServiceRecord(record); + } + + /* + * Checks if Bluetooth device is turned on. + * + * @return true is the Bluetooth device is on, + * false otherwise. + */ + public boolean isPowerOn() { + return BCC.getInstance().isBluetoothEnabled(); + } + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/NameResultEvent.java b/java/midp/com/sun/jsr082/bluetooth/NameResultEvent.java new file mode 100644 index 000000000..e24a2b0df --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/NameResultEvent.java @@ -0,0 +1,64 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; + +/* + * Remote device's friendly name retrieval event. + */ +public class NameResultEvent extends BluetoothEvent { + + /* Bluetooth address of the device. */ + private String deviceAddr; + + /* Friendly name of the device. */ + private String deviceName; + + /* + * Creates this event. + * + * @param addr Bluetooth address of the device + * @param name friendly name of the device + */ + public NameResultEvent(String addr, String name) { + super.eventName = "NameResultEvent"; + deviceAddr = addr; + deviceName = name; + } + + /* + * Processes this event immediately. + */ + public void dispatch() { + BluetoothStack.getInstance().onNameRetrieve(deviceAddr, deviceName); + } + + /* + * Processes this event. Empty, since the processing was done in dispatch(). + */ + public void process() { + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/NativeBCC.java b/java/midp/com/sun/jsr082/bluetooth/NativeBCC.java new file mode 100644 index 000000000..1447d240e --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/NativeBCC.java @@ -0,0 +1,306 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import javax.bluetooth.*; +import com.sun.jsr082.bluetooth.BluetoothStack; +import java.util.Vector; + +/* + * Native-based Bluetooth Control Center. For many operations, this + * implementation relies on BluetoothStack class. + */ +public class NativeBCC extends BCC { + + /* + * Delimiter character used to separate entries in packed strings returned + * by getPreknown(). + */ + private final char ADDR_DELIMETER = ':'; + + + /* + * Constructs the only instance of this class. + */ + protected NativeBCC() { + initialize(); + } + + /* + * Allocates native resources. + */ + private native void initialize(); + + /* + * Releases native resources. + */ + protected native void finalize(); + + /* + * Enables Bluetooth radio and the Bluetooth protocol stack for use. + * + * @return true if the operation succeeded, false otherwise + */ + public boolean enableBluetooth() { + if (BluetoothStack.getInstance().isEnabled()) { + return true; + } + if (!confirmEnable()) { + return false; + } + return BluetoothStack.getInstance().enable(); + } + + /* + * Queries the power state of the Bluetooth device. + * + * @return true is the Bluetooth device is on, + * false otherwise. + */ + public boolean isBluetoothEnabled() { + return BluetoothStack.getInstance().isEnabled(); + } + + /* + * Asks user whether Bluetooth radio is allowed to be turned on. + * + * @return true if user has allowed to enable Bluetooth, false otherwise + */ + public native boolean confirmEnable(); + + /* + * Returns local Bluetooth address. + * + * @return local Bluetooth address. + */ + public String getBluetoothAddress() { + return BluetoothStack.getInstance().getLocalAddress(); + } + + /* + * Determines if the local device is in connectable mode. + * + * @return true if the device is connectable, false otherwise + */ + public native boolean isConnectable(); + + /* + * Returns user-friendly name for the local device. + * + * @return user-friendly name for the local device, or + * null if the name could not be retrieved + * @see LocalDevice#getFriendlyName + */ + public String getFriendlyName() { + return BluetoothStack.getInstance().getLocalName(); + } + + /* + * Retrieves the user-friendly name for specified remote device. + * + * @param address Bluetooth address of a remote device + * @return name of the remote device, or + * null if the name could not be retrieved + * @see RemoteDevice#getFriendlyName + */ + public String getFriendlyName(String address) { + return BluetoothStack.getInstance().askFriendlyNameSync(address); + } + + // JAVADOC COMMENT ELIDED + public DeviceClass getDeviceClass() { + return new DeviceClass(BluetoothStack.getInstance().getDeviceClass()); + } + + // JAVADOC COMMENT ELIDED + public boolean setServiceClasses(int classes) { + return BluetoothStack.getInstance().setServiceClasses(classes); + } + + // JAVADOC COMMENT ELIDED + public int getAccessCode() { + return BluetoothStack.getInstance().getAccessCode(); + } + + // JAVADOC COMMENT ELIDED + public boolean setAccessCode(int accessCode) { + return BluetoothStack.getInstance().setAccessCode(accessCode); + } + + /* + * Checks if the local device has a bond with a remote device. + * + * @param address Bluetooth address of a remote device + * @return true if the two devices were paired, false otherwise + */ + public native boolean isPaired(String address); + + /* + * Checks if a remote device was authenticated. + * + * @param address Bluetooth address of a remote device + * @return true if the device was authenticated, false otherwise + */ + public native boolean isAuthenticated(String address); + + /* + * Checks if a remote device is trusted (authorized for all services). + * + * @param address Bluetooth address of a remote device + * @return true if the device is trusted, false otherwise + */ + public native boolean isTrusted(String address); + + /* + * Checks if connections to a remote device are encrypted. + * + * @param address Bluetooth address of the remote device + * @return true if connections to the device are encrypted, false otherwise + */ + public native boolean isEncrypted(String address); + + /* + * Retrieves PIN code to use for pairing with a remote device. If the + * PIN code is not known, PIN entry dialog is displayed. + * + * @param address the Bluetooth address of the remote device + * @return string containing the PIN code + */ + public native String getPasskey(String address); + + /* + * Initiates pairing with a remote device. + * + * @param address the Bluetooth address of the device with which to pair + * @param pin an array containing the PIN code + * @return true if the device was authenticated, false otherwise + */ + public native boolean bond(String address, String pin); + + /* + * Authenticates remote device. + * + * @param address Bluetooth address of a remote device + * @return true if the device was authenticated, false otherwise + */ + public boolean authenticate(String address) { + if (isAuthenticated(address)) { + return true; + } + if (!isPaired(address)) { + String pin = getPasskey(address); + if (pin == null || !bond(address, pin)) { + return false; + } + } + + return BluetoothStack.getInstance().authenticateSync(address); + } + + /* + * Authorizes a Bluetooth connection. + * + * @param address Bluetooth address of a remote device + * @param handle handle for the service record of the srvice the remote + * device is trying to access + * @return true if authorization succeeded, false otherwise + */ + public native boolean authorize(String address, int handle); + + /* + * Enables or disables encryption of data exchanges. + * + * @param address the Bluetooth address of the remote device + * @param enable indicated whether the encryption needs to be enabled + * @return true if the encryption has been changed, false otherwise + */ + public boolean encrypt(String address, boolean enable) { + if (setEncryption(address, enable)) { + return BluetoothStack.getInstance().encryptSync(address, enable); + } + return false; + } + + /* + * Returns list of preknown devices in a packed string. + * + * @return vector containing preknown devices + */ + public Vector getPreknownDevices() { + return listDevices(getPreknown()); + } + + /* + * Returns list of preknown devices in a packed string. + * + * @return packed string containing preknown devices + */ + private native String getPreknown(); + + /* + * Extracts Bluetooth addresses from a packed string. + * In the packed string, each device entry is a Bluetooth device address + * followed by ADDR_DELIMETER delimiter. + * + * @param packed string containing Bluetooth addresses + * @return Vector containing Bluetooth addresses + */ + private Vector listDevices(String packed) { + if (packed == null || packed.trim().length() == 0) { + return null; + } + + Vector addrs = new Vector(); + int index = 0; + while (index < packed.length()) { + int end = packed.indexOf(ADDR_DELIMETER, index); + if (end == -1) { + end = packed.length(); + } + addrs.addElement(packed.substring(index, end)); + index = end + 1; + } + return addrs; + } + + /* + * Checks if there is a connection to the remote device. + * + * @param address the Bluetooth address of the remote device + * @return true if connection is established with the remote device + */ + public native boolean isConnected(String address); + + /* + * Increases or decreases encryption request counter for a remote device. + * + * @param address the Bluetooth address of the remote device + * @param enable indicated whether the encryption needs to be enabled + * @return true if the encryption needs to been changed, false otherwise + */ + public native boolean setEncryption(String address, boolean enable); + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/RemoteDeviceImpl.java b/java/midp/com/sun/jsr082/bluetooth/RemoteDeviceImpl.java new file mode 100644 index 000000000..22c4eb9cd --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/RemoteDeviceImpl.java @@ -0,0 +1,45 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; + +import javax.bluetooth.RemoteDevice; + +/* + * Extends RemoteDevice class to allow creating + * instances in this package. + */ +final public class RemoteDeviceImpl extends RemoteDevice { + /* + * Creates an instance with address given. + * + * @param address Bluetooth address of the device + * @see javax.bluetooth.RemoteDevice#RemoteDevice(String) + */ + public RemoteDeviceImpl(String address) { + super(address); + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/SDDB.jpp b/java/midp/com/sun/jsr082/bluetooth/SDDB.jpp new file mode 100644 index 000000000..59c3e48a7 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/SDDB.jpp @@ -0,0 +1,238 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; + +import java.util.Enumeration; +import java.util.Hashtable; +import javax.bluetooth.ServiceRegistrationException; +import com.sun.jsr082.bluetooth.ServiceRecordSerializer; +// #ifndef NO_PUSH +import com.sun.jsr082.bluetooth.BluetoothPush; +// #endif + + +/* + * This class represents service discovery database (SDDB). + */ +public final class SDDB { + + /* The only instance of this class. */ + private static SDDB instance = null; + + /* Helper object for ServiveRecord serialization. */ + private ServiceRecordSerializer srs = new ServiceRecordSerializer(); + + /* + * Retrieves the SDDB instance, initializes if needed. + * + * Due to SDP uses java implementation of L2CAPConnection, + * we may initialize SDP only when LocalDevice is initialized, + * and application creates either notifier or connection. + * + * @return the SDDB instance + */ + public static synchronized SDDB getInstance() { + if (instance == null) { + instance = new SDDB(); + // #ifdef NO_PUSH + // initialize SDDB + initialize(); + // #endif + + // #ifdef USE_JSR_82_EMULATOR + // IMPL_NOTE move to proper place when moving + // emulation below porting layer completed + com.sun.midp.jsr82emul.EmulationPolling.start(); + // #endif + } + return instance; + } + + /* + * Creates an instance of this class. + */ + private SDDB() { + } + + /* + * Creates or updates a service record in the SDDB. + * + * For records whose handle is 0, a new entry in SDDB is created. + * Otherwise, an existing entry is updated. + * + * @param record service record to be added to or updated in the database + * @throws ServiceRegistrationException if an error occured while + * updating SDDB + */ + public void updateServiceRecord(ServiceRecordImpl record) + throws ServiceRegistrationException { + int oldHandle = record.getHandle(); + int classes = record.getDeviceServiceClasses(); + if (oldHandle == 0) { + record.setHandle(0); + } + byte[] data = srs.serialize(record); + int newHandle = updateRecord(oldHandle, classes, data); + if (newHandle == 0) { + throw new ServiceRegistrationException( + "Error updating service record."); + } + if (newHandle != oldHandle) { + record.setHandle(newHandle); + } + } + + /* + * Removes record from the database. + * + * @param record the service record that indicates one to be removed from + * the database + */ + public void removeServiceRecord(ServiceRecordImpl record) { + removeRecord(record.getHandle()); + record.setHandle(0); + } + + /* + * Checks if (a copy of) given record is already in the SDDB. + * The record is cosidered to be in the SDDB if the database contains + * a record with the same handle. + * + * @param record the record to be tested + */ + public boolean contains(ServiceRecordImpl record) { + return readRecord(record.getHandle(), null) > 0; + } + + /* + * Retrieves handles of all the records in the database as array. + * + * @return array of int that contains handles of all the service records + * from the database, each once and only them + */ + int[] getHandles() { + int size = getRecords(null); + int handles[] = new int[size]; + getRecords(handles); + return handles; + } + + /* + * Retrieves a service record from the database by the handle value. + * + * @param handle the handle value to get service record by + * @return a reference to the record in the database if one with handle + * given presents, null otherwise + */ + public ServiceRecordImpl getServiceRecord(int handle) { + return getServiceRecord(handle, null); + } + + /* + * Retrieves a service record from the database by the handle value. + * + * @param handle the handle value to get service record by + * @param notifier Bluetooth notifier to be used with the service record + * @return a reference to the record in the database if one with handle + * given presents, null otherwise + */ + public ServiceRecordImpl getServiceRecord(int handle, + BluetoothNotifier notifier) { + int size = readRecord(handle, null); + if (size == 0) { + return null; + } + byte[] data = new byte[size]; + readRecord(handle, data); + ServiceRecordImpl record = srs.restore(notifier, data); + record.setDeviceServiceClasses(getServiceClasses(handle)); + record.setHandle(handle); + return record; + } + + /* + * Creates or updates service record in the SDDB. + * + * @param handle handle of the service record to be updated; + * if equals to 0, a new record will be created + * @param classes device service classes associated with the record + * @param data binary data containing attribute-value pairs in the format + * identical to the one used in the AttributeList parameter of + * the SDP_ServiceAttributeResponse PDU + * @return service record handle, or 0 if the operation fails + */ + public native int updateRecord(int handle, int classes, byte[] data); + + /* + * Removes service record from the SDDB. + * + * @param handle hanlde of the service record to be deleted + */ + public native void removeRecord(int handle); + + /* + * Retrieves service record from the SDDB. + * + * @param handle handle of the service record to be retrieved + * @param data byte array which will receive the data, + * or null for size query + * @return size of the data read/required + */ + private native int readRecord(int handle, byte[] data); + + /* + * Retrieves service classes of a record in the SDDB. + * + * @param handle handle of the service record + * @return service classes set for the record + */ + private native int getServiceClasses(int handle); + + /* + * Retrieves handles of all service records in the SDDB. + * + * @param handles array to receive handles, or null for count query + * @return number of entries read/available + */ + private native int getRecords(int[] handles); + + // #ifdef NO_PUSH + /* + * Native initializer (opens SDDB) + * + * Needed only if there is no push + */ + private static native void initialize(); + + /* + * Native finalizer (closes SDDB) + * + * Needed only if there is no push + */ + protected native void finalize(); + // #endif +} diff --git a/java/midp/com/sun/jsr082/bluetooth/SDP.java b/java/midp/com/sun/jsr082/bluetooth/SDP.java new file mode 100644 index 000000000..5e2375877 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/SDP.java @@ -0,0 +1,88 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import java.io.IOException; +import javax.bluetooth.UUID; +import javax.microedition.io.Connector; +import javax.microedition.io.Connection; +import com.sun.jsr082.bluetooth.btl2cap.Protocol; + +/* + * Contains common Service Discovery Protocol data. + */ +public class SDP { + /* Internal security token that grants access to restricted API. */ +// private static SecurityToken internalSecurityToken = null; + + /* Special object used by SDP to authenticate internal URL's. */ + private static Object systemToken = new Object(); + + /* SDP UUID. */ + public static final String UUID = new UUID(0x0001).toString(); + + /* SDP server PSM. This value is defined by Bluetooth specification */ + public static final int PSM = 0x0001; + + /* + * Initializes internal security token. Called by internal MIDP + * initialization routines. + * + * @param token internal security token that grants access to restricted API + */ +/* + public static void initSecurityToken(SecurityToken token) { + if (internalSecurityToken == null) { + internalSecurityToken = token; + } + } +*/ + /* + * Creates and opens btl2cap connection for using by SDP. + * + * @param name btl2cap connection URL without protocol name + * @return required connection instance, that is + * L2CAPConnection for client or + * L2CAPConnectionNotifier for server + * @exception IOException if connection fails + */ + static Connection getL2CAPConnection(String name) throws IOException { + BluetoothUrl url = new BluetoothUrl(BluetoothUrl.L2CAP, name, systemToken); + Protocol l2cap = new Protocol(); + return l2cap.openPrim(url, Connector.READ_WRITE); + } + + /* + * Checks that given token is an internal one. Used to authenticate + * an URL as generated by SDP. + * @param token token to check + * @return true if the object given is exactly the internal + * system token, false otherwise + */ + public static boolean checkSystemToken(Object token) { + return token == systemToken; + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/SDPClient.java b/java/midp/com/sun/jsr082/bluetooth/SDPClient.java new file mode 100644 index 000000000..180a810f0 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/SDPClient.java @@ -0,0 +1,60 @@ +/* + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; + +import javax.bluetooth.UUID; + +/* + * SDPClient class provides a client side of SDP connection as described in + * Bluetooth Specification version 1.2. + */ +public interface SDPClient { + + /* + * Closes connection of this client to the specified server. + * + * @throws IOException if no connection is open + */ + public void close() throws IOException; + + /* + * Initiates ServiceAttribute transaction that retrieves + * specified attribute values from a specific service record. + */ + public void serviceAttributeRequest(int serviceRecordHandle, int[] attrSet, + int transactionID, SDPResponseListener listener) throws IOException; + + /* + * Initiates ServiceSearchAttribute transaction that searches for services + * on a server by UUIDs specified and retrieves values of specified + * parameters for service records found. + */ + public void serviceSearchAttributeRequest(int[] attrSet, UUID[] uuidSet, + int transactionID, SDPResponseListener listener) throws IOException; + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/SDPClientConnection.java b/java/midp/com/sun/jsr082/bluetooth/SDPClientConnection.java new file mode 100644 index 000000000..b153a4abb --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/SDPClientConnection.java @@ -0,0 +1,149 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.Hashtable; + +import javax.bluetooth.DataElement; +import javax.bluetooth.L2CAPConnection; +import javax.bluetooth.UUID; + +public class SDPClientConnection { + /* L2CAP URL starting string. */ + private static final String SDP_L2CAP_URL_BEGIN = "//"; + + /* L2CAP URL trailing string. */ + private static final String SDP_L2CAP_URL_END = ":0001"; + + private static Hashtable connections = new Hashtable(); + + /* Bluetooth address of the server. */ + private String address; + + /* + * Reference counter keeps the number of SDP connections which + * use this transport. When this value reaches zero, the L2CAP + * connection is closed and the transport is removed from the global + * SDPClient.transportStorage hashtable. + */ + private int refCount = 0; + + /* The L2CAP (logical link) connection. */ + private L2CAPConnection connection; + + /* + * Object that performs reading from and writing to L2CAP connection. + */ + private DataL2CAPReaderWriter rw; + + /* Lock for synchronizing reading from connection. */ + public Object readLock = new Object(); + + /* Lock for synchronizing writing to connection. */ + public Object writeLock = new Object(); + + + /* + * Constructs SDPClientConnection instance that reflects transport + * connections to the specified server. + * + * @param bluetoothAddress bluetooth address of the server + */ + protected SDPClientConnection(String bluetoothAddress) throws IOException { + address = bluetoothAddress; + connection = (L2CAPConnection)SDP.getL2CAPConnection( + SDP_L2CAP_URL_BEGIN + bluetoothAddress + SDP_L2CAP_URL_END); + rw = new DataL2CAPReaderWriter(connection); + } + + public static SDPClientConnection getSDPClientConnection( String bluetoothAddress ) throws IOException { + SDPClientConnection result = null; + synchronized (connections) { + result = (SDPClientConnection)connections.get(bluetoothAddress); + if( result == null ) { + result = new SDPClientConnection( bluetoothAddress ); + connections.put(bluetoothAddress, result); + result.addRef(); + } + } + return result; + } + + public static void closeAll() { + synchronized (connections) { + Enumeration addrs = connections.keys(); + while ( addrs.hasMoreElements() ) { + String addr = (String)addrs.nextElement(); + if (addr != null) { + SDPClientConnection conn = (SDPClientConnection)connections.get(addr); + if (conn != null) { + conn.release(); + } + } + } + connections.clear(); + } + } + + /* + * Increases reference counter. This object and the underlying L2CAP + * connection will live while the counter is positive. + */ + protected synchronized void addRef() { + refCount++; + } + + /* + * Decreases reference counter. If the counter becomes equal to zero, + * L2CAP connection is closed and the transport is removed from the + * global SDPClient.transportStorage hashtable. + */ + public synchronized void release() { + refCount--; + if (refCount <= 0) { + try { + connection.close(); + } catch (IOException e) { + // just ignore it, we're done with this object anyway + } + synchronized (connections) { + connections.remove(this.address); + } + } + } + + public DataL2CAPReaderWriter getReaderWriter() { + return rw; + } + + public L2CAPConnection getL2CAPConnection() { + return connection; + } + + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/SDPClientReceiver.java b/java/midp/com/sun/jsr082/bluetooth/SDPClientReceiver.java new file mode 100644 index 000000000..bbafbdd9d --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/SDPClientReceiver.java @@ -0,0 +1,158 @@ +/* + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.Hashtable; + +public class SDPClientReceiver implements Runnable { + + private static Hashtable receivers = new Hashtable(); + private static int instanceCount = 0; + private JavaSDPClient client = null; + private static final boolean DEBUG= false; + + /* + * Identifies if receiving thread is running (false) or not (true). + */ + private Thread receiverThread = null; + + protected SDPClientReceiver(JavaSDPClient client) { + this.client = client; + receiverThread = new Thread(this); + receiverThread.start(); + if (DEBUG) { + System.out.println("# Receiver internal thread started"); + } + } + + public static synchronized void start(JavaSDPClient client) { + SDPClientReceiver receiver = (SDPClientReceiver)receivers.get(client); + if (receiver == null) { + receiver = new SDPClientReceiver(client); + receivers.put(client, receiver); + instanceCount = 1; + } else { + instanceCount++; + } + if (DEBUG) { + System.out.println("# Receiver[" + instanceCount + "] started"); + } + } + + public static synchronized void stop(JavaSDPClient client) { + SDPClientReceiver receiver = (SDPClientReceiver)receivers.get(client); + if (receiver == null) { + return; + } + if (DEBUG) { + System.out.println("# Receiver[" + instanceCount + "] stopped"); + } + if (--instanceCount > 0) { + return; + } else { + receiver.finish(client.getConnection()); + receivers.remove(client); + } + } + + /* Cancels receiving responses. */ + public static synchronized void cancel() { + Enumeration clientRefs = receivers.keys(); + while( clientRefs.hasMoreElements() ) { + Object ref = clientRefs.nextElement(); + if (ref != null) { + SDPClientReceiver rvr = (SDPClientReceiver)receivers.get(ref); + Thread tmp = rvr.receiverThread; + rvr.finish(((JavaSDPClient)ref).getConnection()); + try { + tmp.join(); + } catch (InterruptedException ie) { + } + if (DEBUG) { + System.out.println("# Receiver internal thread stopped"); + } + + } + } + receivers.clear(); + if (DEBUG) { + System.out.println("# Receiver[all] canceled"); + } + } + + protected synchronized void finish(SDPClientConnection conn) { + if (receiverThread != null) { + Thread tmp = receiverThread; + receiverThread = null; + if (conn != null) { + conn.release(); + if (DEBUG) { + System.out.println("# Receiver: Connection released"); + } + } + } + } + + /* + * The run() method. + * + * @see java.lang.Runnable + */ + public void run() { + Thread current = Thread.currentThread(); + while (client != null && client.getConnection() != null && current == receiverThread ) { + try { + byte pduID = client.getConnection().getReaderWriter().readByte(); + short transID = client.getConnection().getReaderWriter().readShort(); + short length = client.getConnection().getReaderWriter().readShort(); + SDPClientTransaction trans = SDPClientTransaction.findTransaction(transID); + if (trans != null) { + if (DEBUG) { + System.out.println( "# Receiver processResponse:" + trans.getID() ); + } + trans.processResponse(pduID, length); + } else { + if (DEBUG) { + System.out.println("#Receiver transaction: " + transID + " not found"); + } + // transaction we are not aware of; skip this pdu + client.getConnection().getReaderWriter().readBytes(length); + if (current == receiverThread) { + throw new IOException("Invalid transaction id: " + transID); + } + } + } catch (IOException ioe) { + if (current == receiverThread) { + SDPClientTransaction.cancelAll(SDPResponseListener.IO_ERROR); + } + } + } + receiverThread = null; + if (DEBUG) { + System.out.println( "# Receiver internal thread exit" ); + } + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/SDPClientTransaction.java b/java/midp/com/sun/jsr082/bluetooth/SDPClientTransaction.java new file mode 100644 index 000000000..4c9ad500d --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/SDPClientTransaction.java @@ -0,0 +1,352 @@ +/* + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.Hashtable; + +/* + * This abstract class provides base functionality for all SDP + * transactions. + */ +public abstract class SDPClientTransaction extends SDPClientTransactionBase implements Runnable { + + protected static final boolean DEBUG = false; + + /* PDU ID (see Bluetooth Specification 1.2 Vol 3 page 131) */ + byte pduID; + /* Transcation ID used to identify this transaction. */ + int ssTransID; + /* Effective transaction ID. */ + int pduTransID; + /* Length of all parameters. */ + long parameterLength; + /* Continuation state used with partial responses. */ + byte[] continuationState = null; + /* Listener to report request result to. */ + SDPResponseListener listener; + /* Maps transaction IDs to ServiceTransaction objects. */ + private static Hashtable transactions = new Hashtable(); + + protected JavaSDPClient client = null; + + public static SDPClientTransaction findTransaction( int pduTransactionID ) { + SDPClientTransaction result = null; + if (pduTransactionID > 0) { + synchronized (transactions) { + result = (SDPClientTransaction)transactions.get( new Integer( pduTransactionID ) ); + } + } + return result; + } + + /* + * Cancels all current transactions. Called in case of I/O failure + * in the underlying L2CAP connection. + */ + public static void cancelAll(int reason) { + // force actual release of resources + synchronized (transactions) { + Enumeration e = transactions.keys(); + while (e.hasMoreElements()) { + Integer id = (Integer) e + .nextElement(); + if (id != null) { + SDPClientTransaction tr = (SDPClientTransaction)transactions.get(id); + if (tr != null) { + tr.cancel(reason); + } + } + } + transactions.clear(); + } + } + + /* + * Class constructor. + * + * @param pduID protocol data unit ID + * @param ssTransactionID transaction ID of the first request + * @param listener listener object which will receive + * completion and error notifications + */ + public SDPClientTransaction(JavaSDPClient client, int pduID, int ssTransactionID, + SDPResponseListener listener) { + this.pduID = (byte)pduID; + this.ssTransID = ssTransactionID; + pduTransID = newTransactionID(); + this.listener = listener; + this.client = client; + if (DEBUG) { + System.out.println(" Transaction[" + getID() + "] created"); + } + } + + /* + * Updates the effective transaction ID with a new value. + */ + private void updatePduTransactionID() { + synchronized (transactions) { + transactions.remove(new Integer(pduTransID)); + pduTransID = newTransactionID(); + transactions.put(new Integer(pduTransID), this); + } + } + + public String getID() { + return ("" + ssTransID + "_" + pduID); + } + + /* + * Starts this transaction. + * + * @throws IOException when an I/O error occurs + */ + public void run() { + if (client == null) { + return; + } + synchronized (transactions) { + transactions.put( new Integer( pduTransID ), this); + } + continuationState = null; + SDPClientReceiver.start(client); + try { + submitRequest(); + if (DEBUG) { + System.out.println( "Transaction[" + getID() + "]: request sent. Waiting completion" ); + } + synchronized (this) { + wait(); + } + } catch (InterruptedException ie) { + ie.printStackTrace(); + } catch (IOException ioe) { + ioe.printStackTrace(); + } finally { + finish(); + if (DEBUG) { + System.out.println( "Transaction[" + getID() + "] ended" ); + } + } + } + + public void start() { + (new Thread(this)).start(); + } + + /* + * Terminates this transaction and reports error to the listener. + * + * @param reason error code which will be reported + */ + public void cancel(int reason) { + listener.errorResponse(reason, "", ssTransID); + finish(); + if (DEBUG) { + System.out.println( "Transaction[" + getID() + "]: canceled" ); + } + } + + /* + * Ends this transaction by unregistering it in the outer class. + */ + public void finish() { + synchronized (transactions) { + transactions.remove(new Integer(pduTransID)); + } + if (client != null) { + client.removeTransaction(getID()); + } + SDPClientReceiver.stop(client); + synchronized (this) { + notify(); + } + } + + /* + * Reads error PDU, ends this transaction and reports listener the + * error code retrieved. + * + * @param length length of PDU's parameters + */ + public void error(int length) throws IOException { + if (DEBUG) { + System.out.println( "Transaction[" + getID() + "] notify error" ); + } + short errorCode = client.getConnection().getReaderWriter().readShort(); + byte[] infoBytes = client.getConnection().getReaderWriter().readBytes(length - 2); + listener.errorResponse(errorCode, new String(infoBytes), + ssTransID); + finish(); + } + + /* + * Completes the transaction by calling corresponding listener's + * method with the data retrieved. + */ + abstract void complete(); + + /* + * Writes transaction-specific parameters into the PDU. + * + * @throws IOException when an I/O error occurs + */ + abstract void writeParameters() throws IOException; + + /* + * Reads transaction-specific parameters from the PDU. + * + * @param length length of PDU's parameters + * @throws IOException when an I/O error occurs + */ + abstract void readParameters(int length) throws IOException; + + /* + * Gets next SDP server response, if any, and passes it to the + * corresponding listener. If a response is received, the transaction + * it belongs to is stopped. + * + * @throws IOException if an I/O error occurs + */ + boolean processResponse(byte pduID, short length ) throws IOException { + boolean completed = false; + try { + synchronized (client.getConnection().readLock) { + if (DEBUG) { + System.out.println("Transaction[" + getID() + "] pdu_id: " + pduID + " response " + length + " bytes received. processing..."); + } + + if (pduID == SDPClientTransaction.SDP_ERROR_RESPONSE) { + if (DEBUG) { + System.out.println("Transaction[" + getID() + "]: Error response received"); + } + error(length); + return true; + } + if (DEBUG) { + System.out.println("Transaction[" + getID() + "] read parameters..."); + } + readParameters(length); + if (DEBUG) { + System.out.println("Transaction[" + getID() + "] read continuation state..."); + } + completed = readContinuationState(); + if (DEBUG) { + System.out.println("Transaction[" + getID() + "] request should" + (completed ? "n't" : "") + " be resubmitted"); + } + } + continueResponseProcessing(); + } finally { + if (completed) { + synchronized (this) { + notify(); + } + if (DEBUG) { + System.out.println("Transaction[" + getID() + "] finished"); + } + } + } + return completed; +} + +/* + * Processes this transaction by either re-submitting the original + * request if the last response was incomplete, or providing the + * listener with the results if the transaction was completed. + * + * @throws IOException when an I/O error occurs + */ +private void continueResponseProcessing() throws IOException { + if (continuationState != null) { + try { + if (DEBUG) { + System.out.println("Transaction[" + getID() + "] resubmitt request"); + } + resubmitRequest(); + } catch (IOException e) { + if (DEBUG) { + System.out.println("Transaction[" + getID() + "] error: " + e); + } + cancel(SDPResponseListener.IO_ERROR); + throw e; + } + } else { + if (DEBUG) { + System.out.println( "Transaction[" + getID() + "] notify completed" ); + } + complete(); + } +} + +/* + * Re-submits the original request with continuation state + * data received with the incomplete response. + * + * @throws IOException when an I/O error occurs + */ +private void submitRequest() throws IOException { + synchronized (client.getConnection().writeLock) { + client.getConnection().getReaderWriter().writeByte(pduID); + client.getConnection().getReaderWriter().writeShort((short)pduTransID); + client.getConnection().getReaderWriter().writeShort((short)(parameterLength + 1)); + writeParameters(); + if (continuationState != null) { + client.getConnection().getReaderWriter().writeByte((byte)continuationState.length); + client.getConnection().getReaderWriter().writeBytes(continuationState); + } else { + client.getConnection().getReaderWriter().writeByte((byte)0x00); + } + client.getConnection().getReaderWriter().flush(); + if (DEBUG) { + System.out.println("Transaction[" + getID() + "] request " + parameterLength + 1 + " bytes sent"); + } + } +} + +private void resubmitRequest() throws IOException { + updatePduTransactionID(); + submitRequest(); +} + +/* + * Extracts continuation state parameter. + * + * @return true if the continuation state is present, + * false otherwise + * @throws IOException when an I/O error occurs + */ +private boolean readContinuationState() throws IOException { + byte infoLength = client.getConnection().getReaderWriter().readByte(); + if (infoLength == 0) { + continuationState = null; + } else { + continuationState = client.getConnection().getReaderWriter().readBytes(infoLength); + } + return (infoLength == 0); +} + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/SDPClientTransactionBase.java b/java/midp/com/sun/jsr082/bluetooth/SDPClientTransactionBase.java new file mode 100644 index 000000000..bd90e90d6 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/SDPClientTransactionBase.java @@ -0,0 +1,107 @@ +/* + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.Hashtable; + +/* + * This abstract class provides base functionality for all SDP + * transactions. + */ +public abstract class SDPClientTransactionBase { + /* + * Helper object which serializes and restores + * DataElements. + */ + protected DataElementSerializer des = new DataElementSerializer(); + + /* ID of SDP_ErrorResponse protocol data unit. */ + public static final int SDP_ERROR_RESPONSE = 0x01; + + /* ID of SDP_ServiceSearchRequest protocol data unit. */ + public static final int SDP_SERVICE_SEARCH_REQUEST = 0x02; + + /* ID of SDP_ServiceSearchResponse protocol data unit. */ + public static final int SDP_SERVICE_SEARCH_RESPONSE = 0x03; + + /* ID of SDP_ServiceAttributeRequest protocol data unit. */ + public static final int SDP_SERVICE_ATTRIBUTE_REQUEST = 0x04; + + /* ID of SDP_ServiceAttributeResponse protocol data unit. */ + public static final int SDP_SERVICE_ATTRIBUTE_RESPONSE = 0x05; + + /* ID of SDP_ServiceSearchAttributeRequest protocol data unit. */ + public static final int SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST = 0x06; + + /* ID of SDP_ServiceSearchAttributeResponse protocol data unit. */ + public static final int SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE = 0x07; + + /* Max retrievable service record handles. Maybe defined via property. */ + public static final int MAX_SERVICE_RECORD_COUNT = 0x0fff; + + /* Max total size of retrievable attributes. Maybe defined via property. */ + public static final int MAX_ATTRIBUTE_BYTE_COUNT = 0xffff; + + /* + * The lowest possible value of transaction ID. + * The number must be positive. + */ + public static final int firstTransactionID = 0x0001; + + /* The maximum possible value of transaction ID. */ + public static final int maxTransactionID = 0xffff; + + /* Next transaction ID. */ + protected static int effectiveTransactionID = firstTransactionID; + + /* + * Retrieves next new transaction ID. + * + * @return new transaction ID. + */ + public static synchronized short newTransactionID() { + int transactionID = effectiveTransactionID++; + if (effectiveTransactionID > maxTransactionID) { + // strictly speaking, this is not quite safe, + // need revisit : if we have a pending + // transaction after 64K of subsequent calls + effectiveTransactionID = firstTransactionID; + } + return (short)transactionID; + } + + /* + * Frees transaction ID. + * + * @param transactionID the ID to free. + */ + public static synchronized void freeTransactionID(short transactionID) { + // empty in this implementation + } + + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/SDPResponseListener.java b/java/midp/com/sun/jsr082/bluetooth/SDPResponseListener.java new file mode 100644 index 000000000..033ff4c08 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/SDPResponseListener.java @@ -0,0 +1,110 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; +import javax.bluetooth.DataElement; + +/* + * Common interface for SDP server responses listeners. Listenners are assigned + * to active transactions and recieve SDP server responses. + * + */ +public interface SDPResponseListener { + /* Transaction cancelling reason: abnormal IO error. */ + static final int IO_ERROR = 0x010000; + + /* Transaction cancelling reason: transaction has been terminated. */ + static final int TERMINATED = 0x010001; + + /* Error code returned by SDP server: Invalid/unsupported SDP version. */ + static final int SDP_INVALID_VERSION = 0x01; + + /* Error code returned by SDP server: Invalid Service Record Handle. */ + static final int SDP_INVALID_SR_HANDLE = 0x02; + + /* Error code returned by SDP server: Invalid request syntax. */ + static final int SDP_INVALID_SYNTAX = 0x03; + + /* Error code returned by SDP server: Invalid PDU Size. */ + static final int SDP_INVALID_PDU_SIZE = 0x04; + + /* Error code returned by SDP server: Invalid Continuation State. */ + static final int SDP_INVALID_CONTINUATION_STATE = 0x05; + + /* + * Error code returned by SDP server: Insufficient Resources to satisfy + * Request. */ + static final int SDP_INSUFFICIENT_RESOURCES = 0x06; + + /* + * Informs this listener about errors during Service Discovering process. + * + * @param errorCode error code recieved from server or generated locally + * due to transaction terminating. + * + * @param info detail information about the error + * + * @param transactionID ID of transaction response recieved within. + */ + void errorResponse(int errorCode, String info, int transactionID); + + /* + * Informs this listener about found services records. + * + * @param handleList service records handles returned by server within + * SDP_ServiceSearchResponse. + * + * @param transactionID ID of transaction response recieved within. + */ + void serviceSearchResponse(int[] handleList, int transactionID); + + /* + * Informs this listener about found attributes of specified service record. + * + * @param attrIDs list of attributes whose values requested from server + * within SDP_ServiceAttributesRequest. + * + * @param attributeValues values returned by server within + * SDP_ServiceAttributesResponse. + * + * @param transactionID ID of transaction response recieved within. + */ + void serviceAttributeResponse(int[] attrIDs, + DataElement[] attributeValues, int transactionID); + + /* + * Informs this listener about attributes of fisrt found service record. + * + * @param attrIDs list of attributes whose values requested from server + * within SDP_ServiceSearchAttributesRequest. + * + * @param attributeValues values returned by server within + * SDP_ServiceSearchAttributesResponse. + * + * @param transactionID ID of transaction response recieved within. + */ + void serviceSearchAttributeResponse(int[] attrIDs, + DataElement[] attributeValues, int transactionID); +} diff --git a/java/midp/com/sun/jsr082/bluetooth/SelectServiceHandler.java b/java/midp/com/sun/jsr082/bluetooth/SelectServiceHandler.java new file mode 100644 index 000000000..56eb60227 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/SelectServiceHandler.java @@ -0,0 +1,203 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; +import javax.bluetooth.BluetoothStateException; +import javax.bluetooth.DeviceClass; +import javax.bluetooth.DiscoveryAgent; +import javax.bluetooth.DiscoveryListener; +import javax.bluetooth.RemoteDevice; +import javax.bluetooth.ServiceRecord; +import javax.bluetooth.UUID; +import java.util.Hashtable; +import java.util.Vector; + +/* + * This class represents the module which is used by + * DiscoveryAgent#selectService method implementation. + * + */ +final class SelectServiceHandler implements DiscoveryListener { + + /* Set to false in RR version - then the javac skip the code. */ + private static final boolean DEBUG = false; + + private Vector btDevs; + private Hashtable btDevsHash; + private Object btDevsLock = new Object(); + private boolean selectDevDisStarted; + private boolean selectDevDisStopped; + private DiscoveryAgentImpl agent; + + /* + * Constructs SelectServiceHandler for + * DiscoveryAgentImpl given. + * + * @param agent the discovery agent to create instance for. + */ + SelectServiceHandler(DiscoveryAgentImpl agent) { + this.agent = agent; + } + + public void deviceDiscovered(RemoteDevice btDevice, DeviceClass cod) { + + // if this bloototh device was found in preknown or + // cached devices skips it now. + if (btDevsHash.put(btDevice, btDevice) == null) { + btDevs.addElement(btDevice); + } + } + + public void inquiryCompleted(int discType) { + synchronized (btDevsLock) { + selectDevDisStopped = true; + btDevsLock.notify(); + } + } + + public void servicesDiscovered(int transID, ServiceRecord[] servRecord) { + throw new RuntimeException("unexpected call"); + } + + public void serviceSearchCompleted(int transID, int respCode) { + throw new RuntimeException("unexpected call"); + } + + String selectService(UUID uuid, int security, boolean master) + throws BluetoothStateException { + if (DEBUG) { + System.out.println("selectService:"); + System.out.println("\tuuid=" + uuid); + } + Vector disDevsVector = null; + Hashtable disDevsHash = new Hashtable(); + + if (uuid == null) { + throw new NullPointerException("uuid is null"); + } + + // check in CACHED and PREKNOWN devices + String url = selectFromDevicesList(agent.retrieveDevices( + DiscoveryAgent.PREKNOWN), uuid, security, master, disDevsHash); + + if (url != null) { + return url; + } + url = selectFromDevicesList(agent.retrieveDevices( + DiscoveryAgent.CACHED), uuid, security, master, disDevsHash); + + if (url != null) { + return url; + } + + // start own device discovery now + synchronized (btDevsLock) { + if (selectDevDisStarted) { + throw new BluetoothStateException( + "The previous device discovery is running..."); + } + selectDevDisStarted = true; + btDevs = new Vector(); + btDevsHash = disDevsHash; + } + + try { + agent.startInquiry(DiscoveryAgent.GIAC, this); + } catch (BluetoothStateException btse) { + synchronized (btDevsLock) { + selectDevDisStarted = false; + btDevs = null; + btDevsHash = null; + } + throw btse; + } + + synchronized (btDevsLock) { + if (!selectDevDisStopped) { + try { + btDevsLock.wait(); + } catch (InterruptedException ie) { + // ignore (breake waiting) + } + disDevsVector = btDevs; + btDevs = null; + btDevsHash = null; + selectDevDisStarted = false; + selectDevDisStopped = false; + } + } + + for (int i = 0; i < disDevsVector.size(); i++) { + RemoteDevice btDev = (RemoteDevice) disDevsVector.elementAt(i); + url = selectService(btDev, uuid, security, master); + + if (url != null) { + if (DEBUG) { + System.out.println("\turl=" + url); + } + return url; + } + } + if (DEBUG) { + System.out.println("\turl=null"); + } + return null; + } + + private String selectFromDevicesList(RemoteDevice[] devs, UUID uuid, + int security, boolean master, Hashtable disDevsHash) { + if (devs == null) { + return null; + } + + for (int i = 0; i < devs.length; i++) { + if (disDevsHash.put(devs[i], devs[i]) != null) { + continue; + } + String url = selectService(devs[i], uuid, security, master); + + if (url != null) { + if (DEBUG) { + System.out.println("\turl=" + url); + } + return url; + } + } + return null; + } + + private String selectService(RemoteDevice btDev, UUID uuid, int security, + boolean master) { + UUID[] uuidSet = new UUID[] {uuid}; + ServiceSelector selector = new ServiceSelector(null, uuidSet, btDev); + ServiceRecord serRec = selector.getServiceRecord(); + + if (serRec == null) { + return null; + } else { + return serRec.getConnectionURL(security, master); + } + } +} // end of class 'SelectServiceHandler' definition diff --git a/java/midp/com/sun/jsr082/bluetooth/ServiceDiscoverer.java b/java/midp/com/sun/jsr082/bluetooth/ServiceDiscoverer.java new file mode 100644 index 000000000..de060df58 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/ServiceDiscoverer.java @@ -0,0 +1,60 @@ +/* + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import javax.bluetooth.BluetoothStateException; +import javax.bluetooth.DiscoveryListener; +import javax.bluetooth.RemoteDevice; +import javax.bluetooth.UUID; + +public interface ServiceDiscoverer { + /* + * Start searching services under the given conditions + * + * @param attrSet list of attributes whose values are requested. + * @param uuidSet list of UUIDs that indicate services relevant to request. + * @param btDev remote Bluetooth device to listen response from. + * @param discListener discovery listener. + * @throws BluetoothStateException + */ + public int searchService(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev, + DiscoveryListener discListener) throws BluetoothStateException ; + + /* + * Cancels service discovering + * + * @param transID ID of a transaction to be canceled + * @return true if transaction canceled + */ + public boolean cancel(int transID); + + /* + * Returns an SDPClient object and opens SDP connection + * to the remote device with the specified Bluetooth address. + * + * @param bluetoothAddress bluetooth address of SDP server + */ + public SDPClient getSDPClient(String bluetoothAddress); + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/ServiceDiscovererFactory.jpp b/java/midp/com/sun/jsr082/bluetooth/ServiceDiscovererFactory.jpp new file mode 100644 index 000000000..49d240594 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/ServiceDiscovererFactory.jpp @@ -0,0 +1,77 @@ +/* + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import com.sun.j2me.main.Configuration; + +class ServiceDiscovererFactory { + + private static class ServiceDiscovererHolder { + private final static ServiceDiscoverer INSTANCE + = createServiceDiscoverer(); + } + + /* + * Prevents from creating instances of the factory + */ + private ServiceDiscovererFactory() { + } + + /* + * Returns an instance of ServiceDiscoverer + * + * @return ServiceDiscoverer instance + */ + static ServiceDiscoverer getServiceDiscoverer() { + return ServiceDiscovererHolder.INSTANCE; + } + + /* + * Creates an instance of ServiceDiscoverer + */ + private static ServiceDiscoverer createServiceDiscoverer() { + ServiceDiscoverer discoverer = null; + + String serviceDiscovererName = Configuration.getProperty( + "com.sun.jsr082.bluetooth.ServiceDiscoverer"); + if (serviceDiscovererName == null) { + serviceDiscovererName = System.getProperty("com.sun.jsr082.bluetooth.ServiceDiscoverer"); + } + + if (serviceDiscovererName != null) { + try { + Class discClass = Class.forName(serviceDiscovererName); + discoverer = (ServiceDiscoverer)discClass.newInstance(); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + +// #ifdef USE_NATIVE_SDDB + return discoverer == null ? new NativeSDPClient() : discoverer; +// #else + return discoverer == null ? new ServiceSearcher() : discoverer; +// #endif + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/ServiceRecordImpl.java b/java/midp/com/sun/jsr082/bluetooth/ServiceRecordImpl.java new file mode 100644 index 000000000..080ba587a --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/ServiceRecordImpl.java @@ -0,0 +1,643 @@ +/* + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.Hashtable; +import javax.bluetooth.BluetoothStateException; +import javax.bluetooth.DataElement; +import javax.bluetooth.LocalDevice; +import javax.bluetooth.RemoteDevice; +import javax.bluetooth.ServiceRecord; +import javax.bluetooth.UUID; + +import com.sun.jsr082.bluetooth.SDPClient; + +/* + * Service record implementation. + */ +public final class ServiceRecordImpl implements ServiceRecord { + + /* Maxumum quantity of attributes in one request */ + static final int RETRIEVABLE_MAX; + + /* + * Maximum number of concurrent service searches that can + * exist at any one time. + */ + static final int TRANS_MAX; + + /* Remote device service provided by. */ + private RemoteDevice remoteDevice = null; + + /* Service notifier. */ + private BluetoothNotifier notifier = null; + + /* Attribues of the record. */ + private Hashtable attributesTable = null; + + /* Bit scale that keeps service classes. */ + private int serviceClasses = 0; + + /* Mask to identify attribute IDs out of range. */ + private static final int MASK_OVERFLOW = 0xffff0000; + + /* Mask of incorrect class bits. */ + private static final int MASK_INCORRECT_CLASS = 0xff003fff; + + /* ServiceRecordHandle attribute ID. */ + public static final int SERVICE_RECORD_HANDLE = 0x0000; + + /* ProtocolDescriptorList attribute ID. */ + public static final int PROTOCOL_DESCRIPTOR_LIST = 0x0004; + + /* Service class attribute id. */ + public static final int SERVICE_CLASS_ATTR_ID = 0x0001; + + /* Name attribute id. */ + public static final int NAME_ATTR_ID = 0x0100; + + /* Protocol type. */ + private int protocol = BluetoothUrl.UNKNOWN; + + /* Bluetooth address of device service record came from. */ + private String btaddr = null; + + /* PSM or channel id. */ + private int port = -1; + + /* Record handle */ + private int recHandle = 0; + /* SDPClient from where this ServiceRecord is created */ + public SDPClient sdpClient = null; + + static { + int retrievableMax = 5; // default value + try { + retrievableMax = Integer.parseInt(LocalDevice.getProperty( + "bluetooth.sd.attr.retrievable.max")); + } catch (NumberFormatException e) { + System.err.println("Internal error: ServiceRecordImpl: " + + "improper retrievable.max value"); + } + RETRIEVABLE_MAX = retrievableMax; + + int transMax = 10; // default value + try { + transMax = Integer.parseInt(LocalDevice.getProperty( + "bluetooth.sd.trans.max")); + } catch (NumberFormatException e) { + System.err.println("Internal error: ServiceRecordImpl: " + + "improper trans.max value"); + } + TRANS_MAX = transMax; + } + + /* + * Creates service records on client device. + * + * @param device server device + * @param attrIDs attributes IDs + * @param attrValues attributes values + */ + public ServiceRecordImpl(RemoteDevice device, int[] attrIDs, + DataElement[] attrValues) { + init(attrIDs, attrValues); + remoteDevice = device; + } + + + /* + * Creates service records for the given notifier. + * + * @param notifier notifier to be associated with this service record + * @param attrIDs attributes IDs + * @param attrValues attributes values + */ + public ServiceRecordImpl(BluetoothNotifier notifier, int[] attrIDs, + DataElement[] attrValues) { + init(attrIDs, attrValues); + this.notifier = notifier; + } + + /* + * Creates a copy of this record. The copy recieves new instances of + * attributes values which are of types DataElement.DATSEQ + * or DataElement.DATALT (the only data element types that + * can be modified after creation). + * + * @return new instance, a copy of this one. + */ + public synchronized ServiceRecordImpl copy() { + int count = attributesTable.size(); + int[] attrIDs = new int[count]; + DataElement[] attrValues = new DataElement[count]; + + Enumeration ids = attributesTable.keys(); + Enumeration values = attributesTable.elements(); + + for (int i = 0; i < count; i++) { + attrIDs[i] = ((Integer)ids.nextElement()).intValue(); + // no nedd to copy elements here; service record constructor + // performs the copying + attrValues[i] = (DataElement)values.nextElement(); + } + + ServiceRecordImpl servRec = new ServiceRecordImpl(notifier, + attrIDs, attrValues); + servRec.serviceClasses = serviceClasses; + return servRec; + } + + /* + * Returns service record handle. + * + * @return service record handle, or 0 if the record is not in SDDB. + */ + public int getHandle() { + DataElement handle = getAttributeValue(SERVICE_RECORD_HANDLE); + return handle != null ? (int)handle.getLong() : 0; + } + + /* + * Sets service record handle. + * + * @param handle new service record handle value + */ + public void setHandle(int handle) { + Integer attrID = new Integer(SERVICE_RECORD_HANDLE); + attributesTable.remove(attrID); + attributesTable.put(attrID, new DataElement( + DataElement.U_INT_4, handle)); + recHandle = handle; + } + + /* + * Returns notifier that has created this record. + * @return corresponding notifier. + */ + public BluetoothNotifier getNotifier() { + return notifier; + } + + /* + * Creates attributes table and fills it up by values given. + * @param attrIDs attributes IDs + * @param attrValues attributes values + */ + private void init(int[] attrIDs, DataElement[] attrValues) { + attributesTable = new Hashtable(attrIDs.length + 1); + attrsInit(attrIDs, attrValues); + } + + /* + * Fills up attributes table by values given. + * @param attrIDs attributes IDs + * @param attrValues attributes values + */ + private void attrsInit(int[] attrIDs, DataElement[] attrValues) { + for (int i = 0; i < attrIDs.length; i++) { + attributesTable.put(new Integer(attrIDs[i]), + dataElementCopy(attrValues[i])); + } + } + + /* + * Creates a copy of DataElement if it's necessary. + * @param original data element to be copied if its type + * allows value modification + * @return copy of data element + */ + private DataElement dataElementCopy(DataElement original) { + if ((original.getDataType() == DataElement.DATSEQ) + || (original.getDataType() == DataElement.DATALT)) { + DataElement copy = new DataElement(original.getDataType()); + Enumeration elements = (Enumeration) original.getValue(); + + while (elements.hasMoreElements()) { + copy.addElement(dataElementCopy((DataElement) + elements.nextElement())); + } + return copy; + } else { + return original; + } + } + + // JAVADOC COMMENT ELIDED + public DataElement getAttributeValue(int attrID) { + if ((attrID & MASK_OVERFLOW) != 0) { + throw new IllegalArgumentException( + "attrID isn't a 16-bit unsigned integer"); + } + DataElement attrValue = (DataElement) attributesTable.get(new + Integer(attrID)); + + if (attrValue == null) { + return null; + } else { + return dataElementCopy(attrValue); + } + } + + // JAVADOC COMMENT ELIDED + public RemoteDevice getHostDevice() { + return remoteDevice; + } + + // JAVADOC COMMENT ELIDED + public synchronized int[] getAttributeIDs() { + int[] attrIDs = new int[attributesTable.size()]; + Enumeration e = attributesTable.keys(); + + for (int i = 0; i < attrIDs.length; i++) { + attrIDs[i] = ((Integer) e.nextElement()).intValue(); + } + return attrIDs; + } + + // JAVADOC COMMENT ELIDED + public synchronized boolean populateRecord(int[] attrIDs) + throws IOException { + Hashtable dupChecker = new Hashtable(); + Object checkObj = new Object(); + + if (remoteDevice == null) { + throw new RuntimeException("local ServiceRecord"); + } + + if (attrIDs.length == 0) { + throw new IllegalArgumentException("attrIDs size is zero"); + } + + if (attrIDs.length > RETRIEVABLE_MAX) { + throw new IllegalArgumentException( + "attrIDs size exceeds retrievable.max"); + } + + for (int i = 0; i < attrIDs.length; i++) { + if ((attrIDs[i] & MASK_OVERFLOW) != 0) { + throw new IllegalArgumentException("attrID does not represent " + + "a 16-bit unsigned integer"); + } + + // check attribute ID duplication + if (dupChecker.put(new Integer(attrIDs[i]), checkObj) != null) { + throw new IllegalArgumentException( + "duplicated attribute ID"); + } + } + + // obtains transaction ID for request + short transactionID = SDPClientTransactionBase.newTransactionID(); + + // SDP connection and listener. They are initialized in try blok. + SDPClient sdp = null; + SRSDPListener listener = null; + + try { + // prepare data for request + DataElement handleEl = (DataElement) attributesTable.get( + new Integer(SERVICE_RECORD_HANDLE)); + int handle = (int) handleEl.getLong(); + + // create and prepare SDP listner + listener = new SRSDPListener(); + + // create SDP connection and .. + if (sdpClient == null) { + sdp = ServiceDiscovererFactory.getServiceDiscoverer(). + getSDPClient(remoteDevice.getBluetoothAddress()); + } else { + sdp = sdpClient; + } + + // ... and make request + sdp.serviceAttributeRequest(handle, attrIDs, transactionID, + listener); + + synchronized (listener) { + if ((listener.ioExcpt == null) + && (listener.attrValues == null)) { + try { + listener.wait(); + } catch (InterruptedException ie) { + // ignore (breake waiting) + } + } + } + } finally { + + // Closes SDP connection and frees transaction ID in any case + SDPClientTransactionBase.freeTransactionID(transactionID); + + // if connection was created try to close it + if (sdp != null) { + try { + sdp.close(); + } catch (IOException ioe) { + // ignore + } + } + } + + if (listener.ioExcpt != null) { + throw listener.ioExcpt; + } else if (listener.attrValues == null) { + return false; + } else if (listener.attrValues.length == 0) { + return false; + } else { + attrsInit(listener.attrIDs, listener.attrValues); + return true; + } + } + + // JAVADOC COMMENT ELIDED + public synchronized String getConnectionURL(int requiredSecurity, + boolean mustBeMaster) { + // protocol, btaddr, port + retrieveUrlCommonParams(); + if (protocol == BluetoothUrl.UNKNOWN) { + return null; + } + BluetoothUrl url = BluetoothUrl.createClientUrl( + protocol, btaddr, port); + + if (mustBeMaster) { + url.master = true; + } else { + url.master = false; + } + + switch (requiredSecurity) { + case NOAUTHENTICATE_NOENCRYPT: + break; + case AUTHENTICATE_ENCRYPT: + url.encrypt = true; + case AUTHENTICATE_NOENCRYPT: + url.authenticate = true; + break; + default: + throw new IllegalArgumentException("unsupported security type: " + + requiredSecurity); + } + + return url.toString(); + } + + /* + * Retrieves service protocol, device address and port (PSM or channel) + * from service record attributes. Results are set to + * protocol, btaddr and port + * variables correspondingly. + */ + private void retrieveUrlCommonParams() { + if (protocol != BluetoothUrl.UNKNOWN) { + // already retrieved + return; + } + + if (remoteDevice != null) { + btaddr = remoteDevice.getBluetoothAddress(); + } else { + try { + btaddr = LocalDevice.getLocalDevice().getBluetoothAddress(); + } catch (BluetoothStateException bse) { + throw new IllegalArgumentException("cannot generate url"); + } + } + + /* + * There are three protocols supported - + * they are obex or rfcomm or l2cap. So, if obex is + * found in ProtocolDescriptorList, the protocol is btgoep, + * if RFCOMM is found (and no obex) - the btspp, otherwise + * the protocol is btl2cap. + */ + DataElement protocolList = getAttributeValue(PROTOCOL_DESCRIPTOR_LIST); + if (protocolList == null) { + return; + } + Enumeration val = (Enumeration) protocolList.getValue(); + int type = -1; // 0 = l2cap, 1 = spp, 2 = obex + final UUID L2CAP_UUID = new UUID(0x0100); + final UUID RFCOMM_UUID = new UUID(0x0003); + final UUID OBEX_UUID = new UUID(0x0008); + + // go through all of the protocols in the protocols list + while (val.hasMoreElements()) { + DataElement protoDE = (DataElement) val.nextElement(); + + // application adds a garbage in protocolList - ignore + if (protoDE.getDataType() != DataElement.DATSEQ) { + continue; + } + Enumeration protoEnum = (Enumeration) protoDE.getValue(); + int tmpPort = -1; + int tmpType = -1; + + // look on protocol details + while (protoEnum.hasMoreElements()) { + DataElement de = (DataElement) protoEnum.nextElement(); + + // may be PSM or channel id + if (de.getDataType() == DataElement.U_INT_1 || + de.getDataType() == DataElement.U_INT_2) { + tmpPort = (int) de.getLong(); + } else if (de.getDataType() == DataElement.UUID) { + UUID protoUUID = (UUID) de.getValue(); + + if (protoUUID.equals(L2CAP_UUID)) { + tmpType = 0; + } else if (protoUUID.equals(RFCOMM_UUID)) { + tmpType = 1; + } else if (protoUUID.equals(OBEX_UUID)) { + tmpType = 2; + } + } + } + + /* + * ok, new protocol has been parsed - let's check if it + * is over the previous one or not. + * + * Note, that OBEX protocol may appear before the RFCOMM + * one - in this case the port (channel id) is not set - + * need to check this case separately. + */ + if (tmpType > type) { + type = tmpType; + + // no "port" for obex type (obex = 2) + if (tmpType != 2) { + port = tmpPort; + } + } else if (tmpType == 1) { + port = tmpPort; + } + } + + switch (type) { + case 0: + protocol = BluetoothUrl.L2CAP; + break; + case 1: + protocol = BluetoothUrl.RFCOMM; + break; + case 2: + protocol = BluetoothUrl.OBEX; + break; + default: + throw new IllegalArgumentException("wrong protocol list"); + } + } + + /* + * Retrieve service classes bits provided by corresponing service + * at local device. + * + * @return an integer that keeps the service classes bits + */ + public int getDeviceServiceClasses() { + if (remoteDevice != null) { + throw new RuntimeException( + "This ServiceRecord was created by a call to " + + "DiscoveryAgent.searchServices()"); + } + + // it's necessary to improve these code + return serviceClasses; + } + + // JAVADOC COMMENT ELIDED + public synchronized void setDeviceServiceClasses(int classes) { + // checks that it's service record from remote device + if (remoteDevice != null) { + throw new RuntimeException("This ServiceRecord was created" + + " by a call to DiscoveryAgent.searchServices()"); + } + + // checks correction of set classbits + if ((classes & MASK_INCORRECT_CLASS) != 0) { + throw new IllegalArgumentException("attempt to set incorrect bits"); + } + serviceClasses = classes; + } + + // JAVADOC COMMENT ELIDED + public synchronized boolean setAttributeValue( + int attrID, DataElement attrValue) { + + if ((attrID & MASK_OVERFLOW) != 0) { + throw new IllegalArgumentException( + "attrID does not represent a 16-bit unsigned integer"); + } + + if (attrID == SERVICE_RECORD_HANDLE) { + throw new IllegalArgumentException( + "attrID is the value of ServiceRecordHandle (0x0000)"); + } + + if (remoteDevice != null) { + throw new RuntimeException( + "can't update ServiceRecord of the RemoteDevice"); + } + Object key = new Integer(attrID); + + if (attrValue == null) { + return attributesTable.remove(key) != null; + } else { + attributesTable.put(key, dataElementCopy(attrValue)); + return true; + } + } + + /* + * SDP responce listener that is used within populateRecord() + * processing. + */ + class SRSDPListener implements SDPResponseListener { + /* Attributes values retrieved form remote device. */ + DataElement[] attrValues = null; + /* Keeps an IOException to be thrown. */ + IOException ioExcpt = null; + /* IDs of attributes to be retrieved. */ + int[] attrIDs = null; + + /* + * Receives error response. + * @param errorCode error code + * @param info error information + * @param transactionID transaction ID + */ + public void errorResponse(int errorCode, String info, + int transactionID) { + synchronized (this) { + ioExcpt = new IOException(info); + notify(); + } + } + + /* + * Implements required SDPResponseListener method, + * must not be called. + * @param handleList no matter + * @param transactionID no matter + */ + public void serviceSearchResponse(int[] handleList, + int transactionID) { + throw new RuntimeException("unexpected call"); + } + + /* + * Receives arrays of service record attributes and their values. + * @param attributeIDs list of attributes whose values were requested + * from server. + * @param attributeValues values returned by server within. + * @param transactionID ID of transaction response recieved within. + */ + public void serviceAttributeResponse(int[] attributeIDs, + DataElement[] attributeValues, int transactionID) { + synchronized (this) { + attrIDs = attributeIDs; + attrValues = attributeValues; + notify(); + } + } + + /* + * Implements required SDPResponseListener method, + * must not be called. + * @param attrIDs no matter + * @param attributeValues no matter + * @param transactionID no matter + */ + public void serviceSearchAttributeResponse(int[] attrIDs, + DataElement[] attributeValues, int transactionID) { + throw new RuntimeException("unexpected call"); + } + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/ServiceRecordSerializer.java b/java/midp/com/sun/jsr082/bluetooth/ServiceRecordSerializer.java new file mode 100644 index 000000000..74dfe1315 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/ServiceRecordSerializer.java @@ -0,0 +1,182 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; +import java.io.IOException; +import java.util.Enumeration; +import javax.bluetooth.DataElement; +import javax.bluetooth.RemoteDevice; +import javax.bluetooth.ServiceRecord; + +/* + * Serializes ServiceRecord objects. + */ +public class ServiceRecordSerializer { + + /* Helper object used for data element serialization. */ + static DataElementSerializer des; + + /* Initializes static fields. */ + static { + des = new DataElementSerializer(); + } + + /* + * Serializes given service record - creates an array of bytes representing + * data elements as described in Bluetooth Specification Version 1.2, + * vol 3, page 127. + * + * @param record the service record to serialize + * @return an array containing the serialized record + */ + public static synchronized byte[] serialize(ServiceRecord record) { + DataElement seq = new DataElement(DataElement.DATSEQ); + int[] attrIDs = record.getAttributeIDs(); + for (int i = 0; i < attrIDs.length; i++) { + DataElement attrID = new DataElement(DataElement.U_INT_2, + attrIDs[i]); + DataElement attrValue = record.getAttributeValue(attrIDs[i]); + if (attrValue != null) { + seq.addElement(attrID); + seq.addElement(attrValue); + } + } + try { + return des.serialize(seq); + } catch (IOException e) { + return null; + } + } + + /* + * Restores previously serialized service record. + * + * @param notifier notifier object the newly created record + * to be associated with + * @param data serialized service record data + * @return restored service record + */ + public static synchronized ServiceRecordImpl restore( + BluetoothNotifier notifier, byte[] data) { + DataElement seq; + try { + seq = des.restore(data); + } catch (IOException e) { + return null; + } + Enumeration elements = (Enumeration)seq.getValue(); + int[] attrIDs = new int[seq.getSize() / 2]; + DataElement[] attrValues = new DataElement[attrIDs.length]; + for (int i = 0; i < attrIDs.length; i++) { + attrIDs[i] = (int)((DataElement)elements.nextElement()).getLong(); + DataElement attrValue = (DataElement)elements.nextElement(); + DataElement newAttrValue; + int dataType = attrValue.getDataType(); + switch (dataType) { + case DataElement.BOOL: + newAttrValue = new DataElement(attrValue.getBoolean()); + break; + case DataElement.NULL: + newAttrValue = new DataElement(DataElement.NULL); + break; + case DataElement.U_INT_1: + case DataElement.U_INT_2: + case DataElement.U_INT_4: + case DataElement.INT_1: + case DataElement.INT_2: + case DataElement.INT_4: + case DataElement.INT_8: + newAttrValue = new DataElement(dataType, + attrValue.getLong()); + break; + case DataElement.DATALT: + case DataElement.DATSEQ: + Enumeration e = (Enumeration)attrValue.getValue(); + newAttrValue = new DataElement(dataType); + while (e.hasMoreElements()) { + newAttrValue.addElement((DataElement)e.nextElement()); + } + break; + default: + newAttrValue = new DataElement(attrValue.getDataType(), + attrValue.getValue()); + break; + } + attrValues[i] = newAttrValue; + } + return new ServiceRecordImpl(notifier, attrIDs, attrValues); + } + + public static synchronized ServiceRecordImpl restore(RemoteDevice device, byte[] data ) { + DataElement seq; + try { + seq = des.restore(data); + } catch (IOException e) { + return null; + } + Enumeration elements = (Enumeration)seq.getValue(); + int[] attrIDs = new int[seq.getSize() / 2]; + DataElement[] attrValues = new DataElement[attrIDs.length]; + for (int i = 0; i < attrIDs.length; i++) { + attrIDs[i] = (int)((DataElement)elements.nextElement()).getLong(); + DataElement attrValue = (DataElement)elements.nextElement(); + DataElement newAttrValue; + int dataType = attrValue.getDataType(); + switch (dataType) { + case DataElement.BOOL: + newAttrValue = new DataElement(attrValue.getBoolean()); + break; + case DataElement.NULL: + newAttrValue = new DataElement(DataElement.NULL); + break; + case DataElement.U_INT_1: + case DataElement.U_INT_2: + case DataElement.U_INT_4: + case DataElement.INT_1: + case DataElement.INT_2: + case DataElement.INT_4: + case DataElement.INT_8: + newAttrValue = new DataElement(dataType, + attrValue.getLong()); + break; + case DataElement.DATALT: + case DataElement.DATSEQ: + Enumeration e = (Enumeration)attrValue.getValue(); + newAttrValue = new DataElement(dataType); + while (e.hasMoreElements()) { + newAttrValue.addElement((DataElement)e.nextElement()); + } + break; + default: + newAttrValue = new DataElement(attrValue.getDataType(), + attrValue.getValue()); + break; + } + attrValues[i] = newAttrValue; + } + return new ServiceRecordImpl(device, attrIDs, attrValues); + } + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/ServiceSearcher.java b/java/midp/com/sun/jsr082/bluetooth/ServiceSearcher.java new file mode 100644 index 000000000..9bbe9f859 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/ServiceSearcher.java @@ -0,0 +1,600 @@ +/* + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import java.io.IOException; +import javax.bluetooth.BluetoothStateException; +import javax.bluetooth.DataElement; +import javax.bluetooth.DiscoveryListener; +import javax.bluetooth.RemoteDevice; +import javax.bluetooth.UUID; + +import com.sun.jsr082.bluetooth.JavaSDPClient; + +import java.util.Hashtable; +import java.util.Vector; + +/* + * This class saves information about responses to SDP_ServiceSearchRequest and + * SDP_ServiceAttributeRequest requests and provides functionality of + * DiscoveryAgent.serviceSearch using multiple requests via JavaSDPClient (Service + * Discovery Protocol) + * + */ +public class ServiceSearcher extends ServiceSearcherBase implements + ServiceDiscoverer { + + /* Set to false in RR version - then the javac skip the code. */ + private static final boolean DEBUG = false; + + /* this class name for debug. */ + private static final String cn = "ServiceSearcher"; + + /* ID of transaction this listener works in. */ + private short transactionID; + + /* + * Listens for service discovery and is informed as a service is discovered. + */ + private DiscoveryListener discListener; + + /* SDP client to send requests. */ + private JavaSDPClient sdp; + + /* Service records handles retrieved from a server response. */ + private int[] handles; + + /* Number of service records handles processed. */ + private int processedHandle; + + /* Indicates if this listener is inactive. */ + private boolean inactive = false; + + /* Indicates if service search has been canceled. */ + private boolean canceled = false; + + /* Indicates if listener notification has been called. */ + private boolean notified = false; + + /* Current quantity of service discovering requests. */ + private static int requestCounter = 0; + + /* Keeps the references of current service search requests. */ + private static Hashtable searchers = new Hashtable(); + + /* + * Creates ServiceSearcher and save all required info in it. + * + */ + public ServiceSearcher() { + + } + + /* + * Returns an JavaSDPClient object and opens SDP connection + * to the remote device with the specified Bluetooth address. + * + * @param bluetoothAddress bluetooth address of SDP server + */ + public SDPClient getSDPClient(String bluetoothAddress) { + try { + sdp = new JavaSDPClient(bluetoothAddress); + } catch (IOException ioe) { + + } + return (SDPClient)sdp; + } + + /* + * Start searching services under the given conditions + * + * @param attrSet + * list of attributes whose values are requested. + * @param uuidSet + * list of UUIDs that indicate services relevant to request. + * @param btDev + * remote Bluetooth device to listen response from. + * @param discListener + * discovery listener. + */ + public int searchService(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev, + DiscoveryListener discListener) throws BluetoothStateException, + IllegalArgumentException { + + if (DEBUG) { + System.out.println("- serviceSearcher: initializing"); + } + initialize(attrSet, uuidSet, btDev); + + if (discListener == null) { + throw new NullPointerException("DiscoveryListener is null"); + } + this.discListener = discListener; + + return start(); + } + + /* + * Starts SDP_ServiceSearchRequest. + * + * @see JavaSDPClient#serviceSearchRequest + * + * @return ID of transaction that has been initiated by the request. + */ + private int start() throws BluetoothStateException { + if (DEBUG) { + System.out.println("- serviceSearcher: start"); + } + synchronized (ServiceSearcher.class) { + if (requestCounter == ServiceRecordImpl.TRANS_MAX) { + throw new BluetoothStateException( + "Too much concurrent requests"); + } + requestCounter++; + } + transactionID = SDPClientTransaction.newTransactionID(); + searchers.put(new Integer(transactionID), this); + synchronized (this) { + notified = false; + } + + handles = null; + processedHandle = 0; + try { + sdp = new JavaSDPClient(btDev.getBluetoothAddress()); + sdp.serviceSearchAttributeRequest(attrSet, uuidSet, transactionID, + this); + // sdp.serviceSearchRequest(uuidSet, transactionID, this); + } catch (IOException ioe) { + if (DEBUG) { + ioe.printStackTrace(); + } + + synchronized (this) { + stop(); + new NotifyListenerRunner( + DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE); + } + } + + if (DEBUG) { + System.out.println("- serviceSearch: started with transaction: " + + transactionID); + } + return transactionID; + } + + /* + * Receives SDP_ErrorResponse and completes the search request activity by + * error reason. + * + * @param errorCode + * error code form SDP_ErrorResponse. + * @param info + * error details firm SDP_ErrorResponse. + * @param transactionID + * ID of transaction response got within. + */ + public void errorResponse(int errorCode, String info, int transactionID) { + if (DEBUG) { + System.out.println(cn + ".errorResponse: called"); + } + + stop(); + + if ((errorCode == SDP_INVALID_VERSION) + || (errorCode == SDP_INVALID_SYNTAX) + || (errorCode == SDP_INVALID_PDU_SIZE) + || (errorCode == SDP_INVALID_CONTINUATION_STATE) + || (errorCode == SDP_INSUFFICIENT_RESOURCES)) { + notifyListener(DiscoveryListener.SERVICE_SEARCH_ERROR); + System.err.println(info); + } else if (errorCode == SDP_INVALID_SR_HANDLE) { + notifyListener(DiscoveryListener.SERVICE_SEARCH_NO_RECORDS); + System.err.println(info); + } else if (errorCode == IO_ERROR) { + notifyListener(DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE); + } else if (errorCode == TERMINATED) { + new NotifyListenerRunner( + DiscoveryListener.SERVICE_SEARCH_TERMINATED); + } + } + + /* + * Receives array of handles retrieved form SDP_serviceSearchResponse. + * + * @param handleList + * service record handles retrieved from + * SDP_srviceSearchResponse. + * @param transactionID + * ID of transaction response has been received in. + */ + public void serviceSearchResponse(int[] handleList, int transactionID) { + if (DEBUG) { + System.out.println(cn + ".serviceSearchResponse: called"); + } + + // there is no reason to perform response processing if search + // is canceled + if (isCanceled()) { + return; + } + + if (handleList == null || handleList.length == 0) { + stop(); + notifyListener(DiscoveryListener.SERVICE_SEARCH_NO_RECORDS); + return; + } + + synchronized (this) { + handles = handleList; + processedHandle = 0; + } + + try { + // there is no reason to request service attributes if service + // search is canceled + if (isCanceled()) { + return; + } + sdp.serviceAttributeRequest(handles[processedHandle], attrSet, + transactionID, this); + } catch (IOException ioe) { + if (DEBUG) { + ioe.printStackTrace(); + } + stop(); + notifyListener(DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE); + } + } + + /* + * Receives arrays of service record attributes and their values retrieved + * from server response. + */ + public void serviceAttributeResponse(int[] attrIDs, + DataElement[] attributeValues, int transactionID) { + if (DEBUG) { + System.out.println(cn + ".serviceAttributeResponse: called"); + } + + // there is no reason to process service attributes if service + // search is canceled + if (isCanceled()) { + return; + } + + synchronized (this) { + processedHandle++; + } + + if (attributeValues != null) { + ServiceRecordImpl[] serviceRecordSet = new ServiceRecordImpl[1]; + serviceRecordSet[0] = new ServiceRecordImpl(btDev, attrIDs, + attributeValues); + try { + // The spec for DiscoveryAgent.cancelServiceSearch() says: + // "After receiving SERVICE_SEARCH_TERMINATED event, + // no further servicesDiscovered() events will occur + // as a result of this search." + if (isCanceled()) { + return; + } + System.out.println("serviceSearch: notify serviceDiscovered"); + discListener.servicesDiscovered(this.transactionID, + serviceRecordSet); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + if (processedHandle == handles.length) { + stop(); + if (DEBUG) { + System.out + .println("serviceSearch: notify service search completed"); + } + notifyListener(DiscoveryListener.SERVICE_SEARCH_COMPLETED); + return; + } + + try { + // there is no reason to continue attributes discovery if search + // is canceled + if (isCanceled()) { + return; + } + sdp.serviceAttributeRequest(handles[processedHandle], attrSet, + transactionID, this); + } catch (IOException ioe) { + if (DEBUG) { + ioe.printStackTrace(); + } + stop(); + notifyListener(DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE); + } + } + + /* + * Base class method not relevant to this subclass, must never be called. + */ + public void serviceSearchAttributeResponse(int[] attrIDs, + DataElement[] attributeValues, int transactionID) { + if (DEBUG) { + System.out.println(cn + ".serviceSearchAttributeResponse: called"); + } + // there is no reason to process service attributes if service + // search is canceled + if (isCanceled()) { + return; + } + + if (attrIDs == null || attrIDs.length <= 0 || attributeValues == null + || attrIDs.length != attributeValues.length) { + notifyListener(DiscoveryListener.SERVICE_SEARCH_NO_RECORDS); + return; + } + int firstPos = 0; + Vector responceRecords = new Vector(); + for (int i = 1; i < attrIDs.length; i++) { + if (attrIDs[i] == 0) { + responceRecords.addElement(getOneRecord(attrIDs, + attributeValues, firstPos, i)); + firstPos = i; + } + } + responceRecords.addElement(getOneRecord(attrIDs, + attributeValues, firstPos, attrIDs.length)); + + ServiceRecordImpl[] records = new ServiceRecordImpl[responceRecords.size()]; + for (int i=0; itrue indicates the service search has been + * canceled; false the application has not called + * cancel operation + */ + private boolean isCanceled() { + return canceled; + } + + /* + * Cancels transaction with given ID. + * + * @param transactionID + * ID of transaction to be cancelled. + * + * @return false if there is no open transaction with ID given, true + * otherwise. + */ + public boolean cancel(int transactionID) { + ServiceSearcher carrier = (ServiceSearcher) searchers.get(new Integer( + transactionID)); + + if (carrier == null) { + return false; + } else { + return carrier.cancel(); + } + } + + /* + * Notifies the listener that service search has been completed with + * specified response code. + * + * @param respCode + * response code. + */ + private void notifyListener(int respCode) { + // guard against multiple notification calls + synchronized (this) { + if (!notified) { + notified = true; + } else { + return; + } + } + + if (DEBUG) { + String codeStr = "Undefined"; + + switch (respCode) { + case DiscoveryListener.SERVICE_SEARCH_COMPLETED: + codeStr = "SERVICE_SEARCH_COMPLETED"; + break; + + case DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE: + codeStr = "SERVICE_SEARCH_DEVICE_NOT_REACHABLE"; + break; + + case DiscoveryListener.SERVICE_SEARCH_ERROR: + codeStr = "SERVICE_SEARCH_ERROR"; + break; + + case DiscoveryListener.SERVICE_SEARCH_NO_RECORDS: + codeStr = "SERVICE_SEARCH_NO_RECORDS"; + break; + + case DiscoveryListener.SERVICE_SEARCH_TERMINATED: + codeStr = "SERVICE_SEARCH_TERMINATED"; + break; + default: + } + if (DEBUG) { + System.out.println("serviceSearchCompleted:"); + System.out.println("\ttransID=" + transactionID); + System.out.println("\trespCode=" + codeStr); + System.out.println("\tinactive=" + inactive); + } + } + + try { + discListener.serviceSearchCompleted(transactionID, respCode); + } catch (Throwable e) { + e.printStackTrace(); + } + } + + /* + * Runnable for launching notifyListener() in a separate + * thread. + * + * @see #notifyListener(int) + */ + class NotifyListenerRunner implements Runnable { + /* Response code to pass to a listener. */ + int respCode; + + /* + * Constructs Runnable instance to pass single response code, starts a + * thread for that. + * + * @param respCode + * response code value + */ + NotifyListenerRunner(int respCode) { + this.respCode = respCode; + new Thread(this).start(); + } + + /* + * The run() method. + * + * @see java.lang.Runnable + */ + public void run() { + notifyListener(respCode); + } + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/ServiceSearcherBase.java b/java/midp/com/sun/jsr082/bluetooth/ServiceSearcherBase.java new file mode 100644 index 000000000..106cd2ca0 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/ServiceSearcherBase.java @@ -0,0 +1,240 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import javax.bluetooth.RemoteDevice; +import javax.bluetooth.UUID; +import javax.bluetooth.DataElement; + +import com.sun.jsr082.bluetooth.SDPClient; + +import java.util.Enumeration; +import java.util.Hashtable; + +/* + * This class saves information about every service descovery request + * to provide functionality of DiscoveryAgent using multiple requests + * via SDPClient (Service Descovery Protocol) by ServiceSelector + * and ServiceSearcher classes. + */ +abstract public class ServiceSearcherBase implements SDPResponseListener { + + /* + * maximum number of allowed UUIDS in search uuids sequence + */ + private static final int MAX_ALLOWED_UUIDS = 12; + private static final Object FAKE_VALUE = new Object(); + /* Mask to determine an attribute ID out of range. */ + private static final int MASK_OVERFLOW = 0xffff0000; + + /* RemoteDevice whose response to be listened. */ + RemoteDevice btDev; + + /* + * The UUIDs from SDP_ServiceSearchRequest or + * SDP_ServiceSearchAttrbuteRequest. + * + * @see SDPClient#serviceSearchRequest + * @see SDPClient#serviceSearchAttributeRequest + */ + UUID[] uuidSet; + + /* + * Attributes list from SDP_ServiceSearchAttrbuteRequest. + * + * @see SDPClient#serviceSearchAttributeRequest + */ + int[] attrSet; + + /* + * Creates ServiceSearcherBase and save all required info in it. + */ + ServiceSearcherBase() { + super(); + } + + /* + * Creates an instance of ServiceSearcherBase + * + * @param attrSet list of attributes whose values are requested. + * @param uuidSet list of UUIDs that indicate services relevant to request. + * @param btDev remote Bluetooth device to listen response from. + * @param discListener discovery listener. + */ + ServiceSearcherBase(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev) { + super(); + initialize(attrSet, uuidSet, btDev); + } + + /* + * Initializes an instance of ServiceSearcherBase + * + * @param attrSet list of attributes whose values are requested. + * @param uuidSet list of UUIDs that indicate services relevant to request. + * @param btDev remote Bluetooth device to listen response from. + * @param discListener discovery listener. + */ + protected void initialize(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev) + throws IllegalArgumentException, NullPointerException { + + this.btDev = btDev; + this.attrSet = ServiceSearcherBase.extendByStandardAttrs(attrSet); + this.uuidSet = ServiceSearcherBase.removeDuplicatedUuids(uuidSet); + } + + /* + * Informs this listener about errors during Service Discovering process. + * + * @param errorCode error code recieved from server or generated locally + * due to transaction terminating. + * + * @param info detail information about the error + * + * @param transactionID ID of transaction response recieved within. + */ + abstract public void errorResponse(int errorCode, String info, + int transactionID); + + /* + * Informs this listener about found services records. + * + * @param handleList service records handles returned by server within + * SDP_ServiceSearchResponse. + * + * @param transactionID ID of transaction response recieved within. + */ + abstract public void serviceSearchResponse(int[] handleList, + int transactionID); + + /* + * Informs this listener about found attributes of specified service record. + * + * @param attrIDs list of attributes whose values requested from server + * within SDP_ServiceAttributesRequest. + * + * @param attributeValues values returned by server within + * SDP_ServiceAttributesResponse. + * + * @param transactionID ID of transaction response recieved within. + */ + abstract public void serviceAttributeResponse(int[] attrIDs, + DataElement[] attributeValues, int transactionID); + + /* + * Informs this listener about attributes of fisrt found service record. + */ + abstract public void serviceSearchAttributeResponse(int[] attrIDs, + DataElement[] attributeValues, int transactionID); + + public static int[] extendByStandardAttrs( int[] attrSet ) + throws IllegalArgumentException { + /* Extend attributes IDs with standard values from the spec except duplicates */ + Hashtable uniquies = new Hashtable(); + /* appending by user required attributes */ + if (attrSet != null) { + if (attrSet.length <= 0 || attrSet.length > ServiceRecordImpl.RETRIEVABLE_MAX) { + throw new IllegalArgumentException("Invalid attribute set length"); + } + for (int i=0; i 0)) { + /* uuid checking */ + for (int i = 0; i < uuidSet.length; i++) { + + if (uuidSet[i] == null) { + throw new NullPointerException("Invalid UUID. Null"); + } + + /* check UUID duplication */ + if (uniquies.put(uuidSet[i], FAKE_VALUE) != null) { + throw new IllegalArgumentException("Duplicated UUID: " + + uuidSet[i]); + } + } + + uuids = new UUID[uniquies.size()]; + Enumeration keys = uniquies.keys(); + + for (int i = 0; keys.hasMoreElements(); i++) { + uuids[i] = (UUID) keys.nextElement(); + } + } else { + uuids = new UUID[0]; + } + return uuids; + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/ServiceSelector.java b/java/midp/com/sun/jsr082/bluetooth/ServiceSelector.java new file mode 100644 index 000000000..e032aec45 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/ServiceSelector.java @@ -0,0 +1,173 @@ +/* + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth; + +import java.io.IOException; +import javax.bluetooth.RemoteDevice; +import javax.bluetooth.DataElement; +import javax.bluetooth.ServiceRecord; +import javax.bluetooth.UUID; + +import com.sun.jsr082.bluetooth.SDPClient; + +/* + * This class provides functionality of DiscoveryAgent.selectService() + * (see JSR 82 texts for details) using SDP serviceAttribute request. + */ +class ServiceSelector extends ServiceSearcherBase { + + /* Set to false in RR version - then the javac skip the code. */ + private static final boolean DEBUG = false; + + /* this class name for debug. */ + private static final String cn = "ServiceSelector"; + + /* Keeps attributes values retrieved from SDP_ServiceAttributeResponse. */ + private DataElement[] attrValues = null; + + /* Keeps an IOException if any occured or SDP_ErrorResponceRecieved. */ + private IOException ioExcpt = null; + + /* + * Creates ServiceDiscoverer and save all required info in it. + */ + ServiceSelector(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev) { + super(attrSet, uuidSet, btDev); + } + + /* + * Recieves error information retrieved from SDP_ErrorResponse and + * copmpletes the request activity by error reason. + */ + public void errorResponse(int errorCode, String info, int transactionID) { + if (DEBUG) { + System.out.println(cn + ".errorResponse: called"); + } + + synchronized (this) { + ioExcpt = new IOException(info); + notify(); + } + } + + /* + * Base class method not relevant to this subclass, it must never be called. + */ + public void serviceSearchResponse(int[] handleList, int transactionID) { + if (DEBUG) { + System.out.println(cn + ".serviceSearchResponse: unexpected call"); + } + throw new RuntimeException("unexpected call"); + } + + /* + * Base class method not relevant to this subclass, it must never be called. + */ + public void serviceAttributeResponse(int[] attrIDs, + DataElement[] attributeValues, int transactionID) { + if (DEBUG) { + System.out.println(cn + + ".serviceAttributeResponse: unexpected call"); + } + throw new RuntimeException("unexpected call"); + } + + /* + * Receives arrays of service record attributes and their values retrieved + * from SDP_ServiceSearchAttributeResponse. + */ + public void serviceSearchAttributeResponse(int[] attrIDs, + DataElement[] attributeValues, int transactionID) { + if (DEBUG) { + System.out.println(cn + ".serviceSearchAttributeResponse: called"); + } + synchronized (this) { + attrSet = attrIDs; + attrValues = attributeValues; + notify(); + } + } + + /* + * Performs SERVICESEARCHATTRIBUTE transaction and returns newly created + * ServiceRecordImpl instance with attributes and values + * returned by server within SDP_serviceSearchAttributeResponse. + * + * @return newly created ServiceRecordImpl instance with + * attributes and values returned by server if the transaction has completed + * successfully and attributes list retrieved is not empty, + * null otherwise. + */ + ServiceRecord getServiceRecord() { + SDPClient sdp = null; + short transactionID = SDPClientTransactionBase.newTransactionID(); + + try { +// sdp = new JavaSDPClient(btDev.getBluetoothAddress()); + sdp = ServiceDiscovererFactory.getServiceDiscoverer(). + getSDPClient(btDev.getBluetoothAddress()); + + if (sdp != null) { + sdp.serviceSearchAttributeRequest(attrSet, uuidSet, + transactionID, this); + } + } catch (IOException ioe) { + if (DEBUG) { + ioe.printStackTrace(); + } + ioExcpt = ioe; + } + + synchronized (this) { + if (ioExcpt == null && attrValues == null && sdp != null) { + try { + wait(); + } catch (InterruptedException ie) { + // ignore (break waiting) + } + } + } + + try { + if (sdp != null) { + sdp.close(); + } + } catch (IOException ioe) { + if (DEBUG) { + ioe.printStackTrace(); + } + // ignore + } + + if (ioExcpt != null) { + return null; + } else if (attrValues == null) { + return null; + } else if (attrValues.length == 0) { + return null; + } else { + return new ServiceRecordImpl(btDev, attrSet, attrValues); + } + } +} diff --git a/java/midp/com/sun/jsr082/bluetooth/btl2cap/L2CAPConnectionImpl.java b/java/midp/com/sun/jsr082/bluetooth/btl2cap/L2CAPConnectionImpl.java new file mode 100644 index 000000000..6ea39bab9 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/btl2cap/L2CAPConnectionImpl.java @@ -0,0 +1,435 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth.btl2cap; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.Vector; +import javax.bluetooth.L2CAPConnection; +import javax.bluetooth.BluetoothConnectionException; +import com.sun.jsr082.bluetooth.BluetoothConnection; +import com.sun.jsr082.bluetooth.BluetoothUrl; +import com.sun.jsr082.bluetooth.BluetoothUtils; + +/* + * Provides the javax.bluetooth.L2CAPConnection + * connection implemetation. + */ +public class L2CAPConnectionImpl extends BluetoothConnection + implements L2CAPConnection { + /* Static initializer. */ + static { + initialize(); + } + + /* + * Native static class initializer. + */ + private native static void initialize(); + + /* + * Native finalizer. + * Releases all native resources used by this connection. + */ + protected native void finalize(); + + /* + * Stores the address of the remote device connected by this connection. + * The value is set by the constructor. + */ + byte[] remoteDeviceAddress; + + /* Lock object for reading from the socket */ + private final Object readerLock = new Object(); + + /* Lock object for writing to the socket */ + private final Object writerLock = new Object(); + + /* + * Negotiated ReceiveMTU and TransmitMTU. + * 16 high bits is ReceiveMTU, 16 low bits is TransmitMTU. + * + * This packeted value is returned by L2CAPConnectionImpl.connect0 and + * L2CAPNotifierImpl.accept0 methods and decoded by doReceiveMTU + * and doTransmitMTU methods. + */ + int mtus = (((-1) << 16) & 0xFFFF0000) & ((-1) & 0xFFFF); + + /* + * Identifies this connection at native layer, + * -1 if connection is not open. + * + * Note: in real mode this field is accessed only from native code. + */ + private int handle = -1; + + /* The receive MTU for the connection. */ + private int receiveMTU = -1; + + /* The transmit MTU for the connection. */ + private int transmitMTU = -1; + + /* + * Constructs an instance and opens connection. + * + * @param url keeps connection details + * @param mode I/O access mode + * @exception IOException if connection fails + */ + protected L2CAPConnectionImpl(BluetoothUrl url, int mode) + throws IOException { + this(url, mode, null); + } + + /* + * Constructs an instance and + * sets up corresponding native connection handle to it. + * + * @param url keeps connection details + * @param mode I/O access mode + * @param notif corresponding L2CAPNotifierImpl instance + * temporary storing native peer handle + * @exception IOException if connection fails + */ + protected L2CAPConnectionImpl(BluetoothUrl url, + int mode, L2CAPNotifierImpl notif) throws IOException { + super(url, mode); + + if (notif == null) { + remoteDeviceAddress = BluetoothUtils.getAddressBytes(url.address); + doOpen(); + } else { + remoteDeviceAddress = new byte[6]; + System.arraycopy(notif.peerAddress, 0, remoteDeviceAddress, 0, 6); + + setThisConnHandle0(notif); + // copy negotiated MTUs returned by L2CAPNotifierImpl.accept0 + mtus = notif.mtus; + } + + receiveMTU = (mtus >> 16) & 0xFFFF; + transmitMTU = mtus & 0xFFFF; + + // Check whether transmit MTU was increased during connection + // establishment phase. If it was, set original MTU value. + // IMPL_NOTE: pass updated transmit MTU to underlaying Bluetooth stack. + if (url.transmitMTU != -1 && + transmitMTU > url.transmitMTU) { + transmitMTU = url.transmitMTU; + } + + setRemoteDevice(); + } + + /* + * Retrieves native connection handle from temporary storage + * inside L2CAPNotifierImpl instance + * and sets it to this L2CAPConnectionImpl instance. + * + * Note: the method sets native connection handle directly to + * handle field of L2CAPConnectionImpl object. + * + * @param notif reference to corresponding L2CAPNotifierImpl + * instance storing native peer handle + */ + private native void setThisConnHandle0(L2CAPNotifierImpl notif); + + /* + * Retrieves address of remote device on the other side of this connection. + * + * @return remote device address + */ + public String getRemoteDeviceAddress() { + return BluetoothUtils.getAddressString(remoteDeviceAddress); + } + + /* + * Returns ReceiveMTU. + * + * @return receive MTU + * + * @throws IOException if the connection is closed. + */ + public final int getReceiveMTU() throws IOException { + if (isClosed()) { + throw new IOException("Connection is closed"); + } + + return receiveMTU; + } + + /* + * Returns TransmitMTU. + * + * @return transmit MTU + * + * @throws IOException if the connection is closed. + */ + public final int getTransmitMTU() throws IOException { + if (isClosed()) { + throw new IOException("Connection is closed"); + } + + return transmitMTU; + } + + /* + * Sends given bytes to this connection. + * + * Note: the method is non-blocking. + * + * @param data bytes to send. + * + * @throws IOException if either connection is closed or I/O error occured + */ + public void send(byte[] data) throws IOException { + checkOpen(); + checkWriteMode(); + if (data == null) { + throw new NullPointerException("The data is null"); + } + + int len = (data.length < transmitMTU) ? data.length : transmitMTU; + int sentBytes; + + /* + * Multiple threads blocked on write operation may return results + * interleaved arbitrarily. From an application perspective, the + * results would be indeterministic. So "writer locks" are + * introduced for "write" operation to the same socket. + */ + synchronized (writerLock) { + sentBytes = sendData(data, 0, len); + } + + if (sentBytes != len) { + throw new IOException("Data sending failed"); + } + } + + /* + * Receives data from this connection. + * + * Note: The method is blocking. + * + * @param buf byte array to place data received to + * @return amount of bytes received + * @throws IOException if either connection is closed or I/O error occured + */ + public int receive(byte[] buf) throws IOException { + checkOpen(); + checkReadMode(); + if (buf == null) { + throw new NullPointerException("The buffer is null"); + } + if (buf.length == 0) { + return 0; + } + int len = (buf.length > receiveMTU) ? receiveMTU : buf.length; + + /* + * Multiple threads blocked on read operation may + * return results interleaved arbitrarily. From an + * application perspective, the results would be + * indeterministic. So "reader locks" are introduced + * for "read" operation from the same handle. + */ + synchronized (readerLock) { + return receiveData(buf, 0, len); + } + } + + /* + * Checks if there is data to receive without blocking. + * @return true if any data can be retrieved via + * receive() method without blocking. + * @throws IOException if the connection is closed. + */ + public boolean ready() throws IOException { + checkOpen(); + return ready0(); + } + + /* + * Closes this connection. + * @throws IOException if I/O error. + */ + public void close() throws IOException { + synchronized (this) { + if (isClosed()) { + return; + } + resetRemoteDevice(); + } + close0(); + } + + /* + * Receives data from this connection. + * + * Note: the method gets native connection handle directly from + * handle field of L2CAPConnectionImpl object. + * + * @param buf the buffer to read to + * @param off start offset in buf array + * at which the data to be written + * @param size the maximum number of bytes to read, + * the rest of the packet is discarded. + * @return total number of bytes read into the buffer or + * 0 if a zero length packet is received + * @throws IOException if an I/O error occurs + */ + protected int receiveData(byte[] buf, int off, int size) + throws IOException { + return receive0(buf, off, size); + } + + /* + * Receives data from this connection. + * + * Note: the method gets native connection handle directly from + * handle field of L2CAPConnectionImpl object. + * + * @param buf the buffer to read to + * @param off start offset in buf array + * at which the data to be written + * @param size the maximum number of bytes to read, + * the rest of the packet is discarded. + * @return total number of bytes read into the buffer or + * 0 if a zero length packet is received + * @throws IOException if an I/O error occurs + */ + protected native int receive0(byte[] buf, int off, int size) + throws IOException; + + /* + * Sends the specified data to this connection. + * + * Note: the method gets native connection handle directly from + * handle field of L2CAPConnectionImpl object. + * + * @param buf the data to send + * @param off the offset into the data buffer + * @param len the length of the data in the buffer + * @return total number of send bytes, + * or -1 if nothing is send + * @throws IOException if an I/O error occurs + */ + protected int sendData(byte[] buf, int off, int len) throws IOException { + return send0(buf, off, len); + } + + /* + * Sends the specified data to this connection. + * + * Note: the method gets native connection handle directly from + * handle field of L2CAPConnectionImpl object. + * + * @param buf the data to send + * @param off the offset into the data buffer + * @param len the length of the data in the buffer + * @return total number of send bytes, + * or -1 if nothing is send + * @throws IOException if an I/O error occurs + */ + protected native int send0(byte[] buf, int off, int len) throws IOException; + + /* + * Checks if there is data to receive without blocking. + * + * Note: the method gets native connection handle directly from + * handle field of L2CAPConnectionImpl object. + * + * @return true if a packet is present, + * false otherwise + * @throws IOException if any I/O error occurs + */ + private native boolean ready0() throws IOException; + + /* + * Closes client connection. + * + * Note: the method gets native connection handle directly from + * handle field of L2CAPConnectionImpl object. + * + * @throws IOException if any I/O error occurs + */ + private native void close0() throws IOException; + + + /* Opens client connection */ + private void doOpen() throws IOException { + // create native connection object + // Note: the method sets resulting native connection handle + // directly to field handle. + create0(url.receiveMTU, url.transmitMTU, url.authenticate, + url.encrypt, url.master); + + byte[] address = BluetoothUtils.getAddressBytes(url.address); + + try { + // establish connection + mtus = connect0(address, url.port); + } catch (IOException e) { + throw new BluetoothConnectionException( + BluetoothConnectionException.FAILED_NOINFO, + e.getMessage()); + } + } + + /* + * Creates a client connection object. + * + * Note: the method gets native connection handle directly from + * handle field of L2CAPConnectionImpl object. + * + * @param imtu receive MTU or -1 if not specified + * @param omtu transmit MTU or -1 if not specified + * @param auth true if authication is required + * @param enc true indicates + * what connection must be encrypted + * @param master true if client requires to be + * a connection's master + * @throws IOException if any I/O error occurs + */ + private native void create0(int imtu, int omtu, boolean auth, + boolean enc, boolean master) throws IOException; + + /* + * Starts client connection establishment. + * + * Note: the method gets native connection handle directly from + * handle field of L2CAPConnectionImpl object. + * + * @param addr bluetooth address of device to connect to + * @param psm Protocol Service Multiplexor (PSM) value + * @return Negotiated ReceiveMTU and TransmitMTU. + * 16 high bits is ReceiveMTU, 16 low bits is TransmitMTU. + * @throws IOException if any I/O error occurs + */ + private native int connect0(byte[] addr, int psm) throws IOException; + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/btl2cap/L2CAPNotifierImpl.jpp b/java/midp/com/sun/jsr082/bluetooth/btl2cap/L2CAPNotifierImpl.jpp new file mode 100644 index 000000000..670af9a91 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/btl2cap/L2CAPNotifierImpl.jpp @@ -0,0 +1,437 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth.btl2cap; + +import java.io.IOException; +import java.io.InterruptedIOException; +import com.sun.jsr082.bluetooth.BluetoothUrl; +import javax.bluetooth.*; +import java.util.Enumeration; +import com.sun.j2me.app.AppPackage; +// #ifndef NO_PUSH +import com.sun.jsr082.bluetooth.BluetoothPush; +// #endif +import com.sun.jsr082.bluetooth.SDDB; +import com.sun.jsr082.bluetooth.BCC; +import com.sun.jsr082.bluetooth.ServiceRecordImpl; +import com.sun.jsr082.bluetooth.BluetoothNotifier; +import com.sun.jsr082.bluetooth.SDP; + +/* + * Implementation of L2CAPConnectionNotifier. + * An instance is created when + * Connector.open("btltcap://localhost...") + * is called from server application. + */ +public class L2CAPNotifierImpl extends BluetoothNotifier + implements L2CAPConnectionNotifier { + + /* Static initializer. */ + static { + initialize(); + } + + /* + * Native static class initializer. + */ + private native static void initialize(); + + /* + * Native finalizer. + * Releases all native resources used by this connection. + */ + protected native void finalize(); + + /* + * Identidies this connection at native layer, + * -1 if connection is not open. + */ + private int handle = -1; + + // IMPL_NOTE make it back private when moving emulation + // below porting layer completed + /* + * Temporary stores peer's native handle for accepted connection. + * Used by doAccept method. + */ + int peerHandle = -1; + + /* + * Temporary stores remote device address for accepted connection. + */ + byte[] peerAddress = new byte[6]; + + /* Indicates whether notifier is listening for incoming connections. */ + protected boolean isListenMode = false; + + /* + * Negotiated ReceiveMTU and TransmitMTU. + * 16 high bits is ReceiveMTU, 16 low bits is TransmitMTU. + * + * This packeted value is returned by accept0 method and copied to + * L2CAPConnectionImpl by it's constructor. + */ + int mtus = (((-1) << 16) & 0xFFFF0000) & ((-1) & 0xFFFF); + + /* Keeps PSM value for service record validation. */ + private DataElement PSM; + + /* Keeps L2CAP UUID for service record validation. */ + static final DataElement L2CAP_UUID = + new DataElement(DataElement.UUID, new UUID(0x0100)); + + /* Bluetooth PushRegistry handle, used in native methods only. */ + private int pushHandle = 0; + + /* + * Creates instance of L2CAPNotifierImpl. + * + * @param url BluetoothUrl that represents server + * connection string to create notifier for + * @param mode I/O access mode + * @throws IOException if an I/O error occurs + */ + protected L2CAPNotifierImpl(BluetoothUrl url, int mode) throws IOException { + super(url, mode); + + AppPackage app = AppPackage.getInstance(); + + int appId = (app != null) ? app.getId() : 0; + String connUrl = "btl2cap:" + url.caseSensitiveUrl; + + // check whether suiteId is valid. + // for example, it can be null if L2CAPNotifier is created as SDP server + // in PushRegistry inbound connection watcher thread that can be started + // before MIDlet start. +// #ifndef NO_PUSH + if ((app != null) && pushCheckout(connUrl, appId)) { + serviceRec = BluetoothPush.getServiceRecord(this, connUrl); + checkServiceRecord(); + } else { + serviceRec = createServiceRecord(this, url, doCreate(url)); + } +// #else + serviceRec = createServiceRecord(this, url, doCreate(url)); +// #endif + } + + /* + * Checks out (takes ownership of) an active server connection maintained + * by push subsystem. + * + * @param url URL used during registration of the push entry + * @param suiteId suite id + * @return true if the operation succeeds, false otherwise + */ +// #ifndef NO_PUSH + private native boolean pushCheckout(String url, int suiteId); +// #endif + + + /* + * Creates an empty service record for the given URL and PSM value. + * + * @param notifier L2CAP notifier object to be associated with the record + * @param url URL from which a new record is constructed + * @param psm PSM value assigned to the notifier + * @return a new service record instance + */ + public static ServiceRecordImpl createServiceRecord( + L2CAPNotifierImpl notifier, BluetoothUrl url, int psm) { + DataElement serviceList = new DataElement(DataElement.DATSEQ); + serviceList.addElement(new DataElement(DataElement.UUID, + new UUID(url.uuid, false))); + DataElement protocolList = new DataElement(DataElement.DATSEQ); + DataElement protocol = new DataElement(DataElement.DATSEQ); + protocol.addElement(L2CAP_UUID); + if (notifier != null) { + notifier.PSM = new DataElement(DataElement.U_INT_2, psm); + protocol.addElement(notifier.PSM); + } else { + protocol.addElement(new DataElement(DataElement.U_INT_2, psm)); + } + protocolList.addElement(protocol); + int[] attrIDs; + DataElement[] attrVals; + if (url.name != null) { + DataElement name = new DataElement(DataElement.STRING, url.name); + attrIDs = new int[] { + ServiceRecordImpl.SERVICE_CLASS_ATTR_ID, + ServiceRecordImpl.PROTOCOL_DESCRIPTOR_LIST, + ServiceRecordImpl.NAME_ATTR_ID + }; + attrVals = new DataElement[] { serviceList, protocolList, name }; + } else { + attrIDs = new int[] { + ServiceRecordImpl.SERVICE_CLASS_ATTR_ID, + ServiceRecordImpl.PROTOCOL_DESCRIPTOR_LIST + }; + attrVals = new DataElement[] { serviceList, protocolList }; + } + return new ServiceRecordImpl(notifier, attrIDs, attrVals); + } + + /* + * Creates an empty service record for the given URL. + * + * @param url URL from which a new record is constructed + * @return a new service record instance + */ + public static ServiceRecordImpl createServiceRecord(String url) { + return createServiceRecord(null, new BluetoothUrl(url), 0); + } + + /* + * Ensures that the service record is valid. + * + * @throws ServiceRegistrationException in case described in the + * JSR specification + */ + protected void checkServiceRecord() throws ServiceRegistrationException { + synchronized (serviceRec) { + // check if the ServiceClassIDList is not missing + if (serviceRec.getAttributeValue( + ServiceRecordImpl.SERVICE_CLASS_ATTR_ID) == null) { + throw new ServiceRegistrationException( + "ServiceClassIDList is missing."); + } + // check the ProtocolList is not missed + DataElement protocolList = serviceRec.getAttributeValue( + ServiceRecordImpl.PROTOCOL_DESCRIPTOR_LIST); + if (protocolList == null) { + throw new ServiceRegistrationException( + "ProtocolDescriptorList is missing."); + } + Enumeration protocolListEnum = + (Enumeration)protocolList.getValue(); + while (protocolListEnum.hasMoreElements()) { + Enumeration protocolEnum = (Enumeration)((DataElement) + protocolListEnum.nextElement()).getValue(); + // check that the L2CAP UUID is not missing, check the PSM + // is not missing and the value has not changed + if (protocolEnum.hasMoreElements()) { + if (compareDataElements((DataElement)protocolEnum. + nextElement(), L2CAP_UUID)) { + if (!protocolEnum.hasMoreElements()) { + throw new ServiceRegistrationException( + "PSM value is missing."); + } + if (PSM == null) { + PSM = new DataElement(DataElement.U_INT_2, + ((DataElement)protocolEnum. + nextElement()).getLong()); + return; + } + if (!compareDataElements((DataElement)protocolEnum. + nextElement(), PSM)) { + throw new ServiceRegistrationException( + "PSM value has changed."); + } + return; + } + } + } + } + throw new ServiceRegistrationException("L2CAP UUID is missing."); + } + + /* + * Ensures that this notifier can accept connections. + * + * @throws IOException if notifier is closed, device is not + * in connectable mode, or service record is invalid + */ + private void ensureConnectable() throws IOException { + if (isClosed) { + throw new BluetoothConnectionException( + BluetoothConnectionException.FAILED_NOINFO, + "Notifier is closed."); + } + if (!BCC.getInstance().isConnectable()) { + throw new BluetoothStateException("The device is not connectable."); + } + checkServiceRecord(); + } + + /* + * Accepts connections blocking until incoming client connection appears. + * + * @return connection to a client just accepted. + * + * @throws IOException if notifier is closed or device is not + * in connectable mode. + */ + public L2CAPConnection acceptAndOpen() throws IOException { + ensureConnectable(); + + // switch on listen mode if it has not been done yet + doListen(); + + // adds the service record if it is not yet in SDDB + SDDB.getInstance().updateServiceRecord(serviceRec); + + L2CAPConnection client; + do { + ensureConnectable(); + + // accept incoming connections if any + client = doAccept(); + } while (client == null); + + return client; + } + + /* + * Closes this notifier making corresponding service record inaccessible. + * updates the information on the communication server. + * + * @exception IOException if an error occured lower in Bluetooth stack. + */ + public void close() throws IOException { + if (isClosed) { + return; + } + isClosed = true; + + SDDB.getInstance().removeServiceRecord(serviceRec); + + doClose(); + } + + /* + * Force listen mode by calling underlying stack methods. + * + * @throws IOException if an error occured + */ + private void doListen() throws IOException { + // force listening if it had not been done yet + if (!isListenMode) { + listen0(); + + isListenMode = true; + } + } + + /* + * Force Bluetooth stack to listen for incoming client connections. + * + * Note: the method gets native connection handle directly from + * handle field of L2CAPNotifierImpl object. + * + * @throws IOException if an I/O error occurs + */ + private native void listen0() throws IOException; + + + + /* + * Advertises acception by calling underlying stack methods. + * + * @return L2CAPConnectionImpl instance to work with accepted client + */ + protected L2CAPConnection doAccept() throws IOException { + if (!isListenMode) { + throw new BluetoothStateException("Device is not in listen mode"); + } + + /* + * Note: native handle is set to peerHandleID field directly by accept0 + * method and retrieved by L2CAPConnectionImpl constructor. + */ + mtus = accept0(); + + return new L2CAPConnectionImpl(url, mode, this); + } + + /* + * Accepts incoming client connection request. + * + * Note: the method gets native connection handle directly from + * handle field of L2CAPNotifierImpl object. + * + * Note: new native connection handle to work with accepted incoming + * client connection is setted directly to handle field of + * appropriate L2CAPConnectionImpl object. + * + * @return Negotiated ReceiveMTU and TransmitMTU. + * 16 high bits is ReceiveMTU, 16 low bits is TransmitMTU. + * @throws IOException if an I/O error occurs + */ + protected native int accept0() throws IOException; + + + /* + * Closes this notifier at native layer. + */ + protected void doClose() throws IOException { + close0(); + } + /* + * Closes this server connection. + * Releases all native resources (such as sockets) owned by this notifier. + * + * Note: the method gets native connection handle directly from + * handle field of L2CAPNotifierImpl object. + * + * @throws IOException IOException if an I/O error occurs + */ + private native void close0() throws IOException; + + + /* + * Creates an instanse of server connection at native layer. + * + * @param url BluetoothUrl that represents server + * connection string to create notifier for + * @return selected psm value to listen for incoming connections on + */ + private int doCreate(BluetoothUrl url) throws IOException { + return create0(url.receiveMTU, url.transmitMTU, url.authenticate, + url.authorize, url.encrypt, url.master); + } + + /* + * Creates a server connection. + * + * Note: the method sets native connection handle directly to + * handle field of L2CAPNotifierImpl object. + * + * @param imtu receive MTU or -1 if not specified + * @param omtu transmit MTU or -1 if not specified + * @param auth true if authication is required + * @param authz true if authorization is required + * @param enc true indicates + * what connection must be encrypted + * @param master true if client requires to be + * a connection's master + * @return selected psm value to listen for incoming connections on + * @throws IOException IOException if an I/O error occurs + */ + private native int create0(int imtu, int omtu, boolean auth, boolean authz, + boolean enc, boolean master) throws IOException; + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/btl2cap/Protocol.java b/java/midp/com/sun/jsr082/bluetooth/btl2cap/Protocol.java new file mode 100644 index 000000000..4155899a8 --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/btl2cap/Protocol.java @@ -0,0 +1,161 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +package com.sun.jsr082.bluetooth.btl2cap; + +import java.io.IOException; +import javax.microedition.io.Connection; +import javax.bluetooth.L2CAPConnection; +import javax.bluetooth.BluetoothConnectionException; + +import com.sun.jsr082.bluetooth.BluetoothUrl; +import com.sun.jsr082.bluetooth.BluetoothProtocol; +import com.sun.j2me.security.BluetoothPermission; +import com.sun.j2me.main.Configuration; + +/* + * Provides "btl2cap" protocol implementation + */ +public class Protocol extends BluetoothProtocol { + /* Keeps maximum MTU supported by the BT stack. */ + static final int MAX_STACK_MTU; + + static { + int maxReceiveMTU; + try { + maxReceiveMTU = Integer.parseInt(System.getProperty( + "bluetooth.l2cap.receiveMTU.max")); + } catch (NumberFormatException e) { + maxReceiveMTU = L2CAPConnection.DEFAULT_MTU; + } + MAX_STACK_MTU = maxReceiveMTU; + } + + /* + * Constructs an instance. + */ + public Protocol() { + super(BluetoothUrl.L2CAP); + } + + /* + * Cheks permissions and opens requested connection. + * + * @param token security token passed by calling class + * @param url BluetoothUrl instance that defines required + * connection stringname the URL without protocol name and colon + * @param mode Connector.READ_WRITE or Connector.READ or Connector.WRITE + * + * @return a notifier in case of server connection string, open connection + * in case of client one. + * + * @exception IOException if opening connection fails. + */ + public Connection openPrim(BluetoothUrl url, int mode) + throws IOException { + return openPrimImpl(url, mode); + } + + /* + * Ensures URL parameters have valid values. Sets receiveMTU if undefined. + * @param url URL to check + * @exception IllegalArgumentException if invalid url parameters found + * @exception BluetoothConnectionException if url parameters are not + * acceptable due to Bluetooth stack limitations + */ + protected void checkUrl(BluetoothUrl url) + throws IllegalArgumentException, BluetoothConnectionException { + + if (url.receiveMTU == -1) { + url.receiveMTU = L2CAPConnection.DEFAULT_MTU; + } + + if (url.isSystem()) { + return; + } + + super.checkUrl(url); + + if (!url.isServer && (url.port <= 0x1000 || url.port >= 0xffff || + ((url.port & 1) != 1) || ((url.port & 0x100) != 0))) { + throw new IllegalArgumentException("Invalid PSM: " + url.port); + } + + // IMPL_NOTE BluetoothConnectionException should be thrown here + // It is temporary substituted by IllegalArgumentException + // to pass TCK succesfully. To be changed back when fixed + // TCK arrives. The erroneous TCK test is + // javasoft.sqe.tests.api.javax.bluetooth.Connector.L2Cap. + // openClientTests.L2Cap1014() + // + // Correct code here is + // throw new BluetoothConnectionException( + // BluetoothConnectionException.UNACCEPTABLE_PARAMS, + // ); + if (url.receiveMTU < L2CAPConnection.MINIMUM_MTU) { + throw new IllegalArgumentException( + "Receive MTU is too small"); + } + + if (url.receiveMTU > MAX_STACK_MTU) { + throw new IllegalArgumentException("Receive MTU is too large"); + } + + if (url.transmitMTU != -1 && url.transmitMTU > MAX_STACK_MTU) { + throw new BluetoothConnectionException( + BluetoothConnectionException.UNACCEPTABLE_PARAMS, + "Transmit MTU is too large"); + } + } + + /* + * Ensures that permissions are proper and creates client side connection. + * @param token security token if passed by caller, or null + * @param mode I/O access mode + * @return proper L2CAPConnectionImpl instance + * @exception IOException if openning connection fails. + */ + protected Connection clientConnection(int mode) + throws IOException { + checkForPermission(BluetoothPermission.BLUETOOTH_CLIENT); + return new L2CAPConnectionImpl(url, mode); + } + + /* + * Ensures that permissions are proper and creates required notifier at + * server side. + * @param token security token if passed by caller, or null + * @param mode I/O access mode + * @return proper L2CAPNotifierImpl instance + * @exception IOException if openning connection fails + */ + protected Connection serverConnection(int mode) + throws IOException { + checkForPermission(BluetoothPermission.BLUETOOTH_SERVER); + return new L2CAPNotifierImpl(url, mode); + } +} + diff --git a/java/midp/com/sun/jsr082/bluetooth/btspp/BTSPPConnectionImpl.java b/java/midp/com/sun/jsr082/bluetooth/btspp/BTSPPConnectionImpl.java new file mode 100644 index 000000000..c9d86d83e --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/btspp/BTSPPConnectionImpl.java @@ -0,0 +1,530 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth.btspp; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InterruptedIOException; +import javax.microedition.io.StreamConnection; +import javax.bluetooth.BluetoothConnectionException; +import com.sun.jsr082.bluetooth.BluetoothUrl; +import com.sun.jsr082.bluetooth.BluetoothUtils; +import com.sun.jsr082.bluetooth.BluetoothConnection; + +/* + * Bluetooth Serial Port Profile connection implementation. + */ +public class BTSPPConnectionImpl extends BluetoothConnection + implements StreamConnection { + + /* Static initializer. */ + static { + initialize(); + } + + /* + * Native static class initializer. + */ + private native static void initialize(); + + /* + * Native finalizer. + * Releases all native resources used by this connection. + */ + protected native void finalize(); + + /* + * Stores the address of the remote device connected by this connection. + * The value is set by the constructor. + */ + byte[] remoteDeviceAddress; + + /* Lock object for reading from the socket */ + private final Object readerLock = new Object(); + + /* Lock object for writing to the socket */ + private final Object writerLock = new Object(); + + /* + * Identidies this connection at native layer, -1 + * if connection is not open. + */ + private int handle = -1; + + /* Flag to identify if an input stream is opened for this connection. */ + private boolean isOpened = false; + + /* Flag to identify if an output stream is opened for this connection. */ + private boolean osOpened = false; + + /* Open streams counter. */ + private int objects = 1; + + /* + * Constructs an instance and opens connection. + * + * @param url keeps connection details + * @param mode I/O access mode + * @exception IOException if connection fails + */ + protected BTSPPConnectionImpl(BluetoothUrl url, int mode) + throws IOException { + this(url, mode, null); + } + + /* + * Constructs an instance and + * sets up corresponding native connection handle to it. + * + * @param url keeps connection details + * @param mode I/O access mode + * @param notif corresponding BTSPPNotifierImpl instance + * temporary storing native peer handle + * @exception IOException if connection fails + */ + protected BTSPPConnectionImpl(BluetoothUrl url, + int mode, BTSPPNotifierImpl notif) throws IOException { + super(url, mode); + + if (notif == null) { + remoteDeviceAddress = BluetoothUtils.getAddressBytes(url.address); + doOpen(); + } else { + remoteDeviceAddress = new byte[6]; + System.arraycopy(notif.peerAddress, 0, remoteDeviceAddress, 0, 6); + + setThisConnHandle0(notif); + } + + setRemoteDevice(); + } + + /* + * Retrieves native connection handle from temporary storage + * inside BTSPPNotifierImpl instance + * and sets it to this BTSPPConnectionImpl instance. + * + * Note: the method sets native connection handle directly to + * handle field of BTSPPConnectionImpl object. + * + * @param notif reference to corresponding BTSPPNotifierImpl + * instance storing native peer handle + */ + private native void setThisConnHandle0(BTSPPNotifierImpl notif); + + /* + * Retrieves remote address for the connection. + * + * @return remote address + */ + public String getRemoteDeviceAddress() { + return BluetoothUtils.getAddressString(remoteDeviceAddress); + } + + /* + * Open and return a data input stream for a connection. + * + * @return An input stream + * @throws IOException if an I/O error occurs + */ + public final DataInputStream openDataInputStream() throws IOException { + return new DataInputStream(openInputStream()); + } + + /* + * Open and return a data output stream for a connection. + * + * @return An output stream + * @throws IOException if an I/O error occurs + */ + public final DataOutputStream openDataOutputStream() throws IOException { + return new DataOutputStream(openOutputStream()); + } + + /* + * Open and return an input stream for a connection. + * + * @return An input stream + * @throws IOException if an I/O error occurs + */ + public final InputStream openInputStream() throws IOException { + checkOpen(); + checkReadMode(); + synchronized (this) { + if (isOpened) { + throw new IOException("No more input streams"); + } + isOpened = true; + objects++; + } + return new SPPInputStream(); + } + + /* + * Open and return an output stream for a connection. + * + * @return An output stream + * @throws IOException if an I/O error occurs + */ + public final OutputStream openOutputStream() throws IOException { + checkOpen(); + checkWriteMode(); + synchronized (this) { + if (osOpened) { + throw new IOException("No more output streams"); + } + osOpened = true; + objects++; + } + return new SPPOutputStream(); + } + + /* + * Input stream implementation for BTSPPConnection + */ + private final class SPPInputStream extends InputStream { + /* Indicates whether the stream is closed. */ + private boolean isClosed; + + /* + * Reads the next byte of data from the input stream. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + */ + public int read() throws IOException { + byte[] buf = new byte[1]; + int res = read(buf); + if (res != -1) + res = buf[0] & 0xFF; + return res; + } + + /* + * Reads some number of bytes from the input stream and stores them into + * the buffer array buf. + * + * @param buf the buffer into which the data is read. + * @return the total number of bytes read into the buffer, or + * -1 is there is no more data because the end + * of the stream has been reached. + * @exception IOException if an I/O error occurs. + * @see java.io.InputStream#read(byte[], int, int) + */ + public int read(byte[] buf) throws IOException { + return read(buf, 0, buf.length); + } + + /* + * Reads up to len bytes of data from the input stream into + * an array of bytes. An attempt is made to read as many as + * len bytes, but a smaller number may be read, possibly + * zero. The number of bytes actually read is returned as an integer. + * @param buf the buffer into which the data is read. + * @param off the start offset in array buf + * at which the data is written. + * @param len the maximum number of bytes to read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end + * of the stream has been reached. + * @exception IOException if an I/O error occurs. + * @see java.io.InputStream#read() + */ + public int read(byte[] buf, int off, int len) throws IOException { + if (isClosed) { + throw new IOException("Stream is closed"); + } + + if ((off < 0) || (len < 0) || (off + len > buf.length)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } + + int res; + + /* + * Multiple threads blocked on read operation may + * return results interleaved arbitrarily. From an + * application perspective, the results would be + * indeterministic. So "reader locks" are introduced + * for "read" operation from the same handle. + */ + synchronized (readerLock) { + res = receive0(buf, off, len); + } + + // convert the 'end of data' to the 'end of stream' + if (res == 0) { + res = -1; + } + + return res; + } + + /* + * Returns the number of bytes that can be read (or skipped over) from + * this input stream without blocking by the next caller of a method for + * this input stream. The next caller might be the same thread or + * another thread. + * + * @return the number of bytes that can be read from this input + * stream without blocking. + * @exception IOException if an I/O error occurs. + */ + public int available() throws IOException { + if (isClosed) { + throw new IOException("Stream is closed"); + } + return available0(); + } + + /* + * Closes this input stream and releases any system resources associated + * with the stream. + * + * @exception IOException if an I/O error occurs. + */ + public void close() throws IOException { + synchronized (BTSPPConnectionImpl.this) { + if (isClosed) { + return; + } + isClosed = true; + objects--; + if (objects == 0) { + close0(); + } + } + } + } + + /* + * Output stream implementation for BTSPPConnection + */ + private final class SPPOutputStream extends OutputStream { + /* Indicates whether the stream is closed. */ + private boolean isClosed; + + /* + * Writes the specified byte to this output stream. + * @param b the byte. + * @exception IOException if an I/O error occurs. In particular, + * an IOException may be thrown if the + * output stream has been closed. + */ + public void write(int b) throws IOException { + write(new byte[] { (byte)b }); + } + + /* + * Writes size bytes from the specified byte array + * starting at offset offset to this output stream. + * @param buf the data. + * @param offset the start offset in the data. + * @param size the number of bytes to write. + * @exception IOException if an I/O error occurs. In particular, + * an IOException is thrown if the output + * stream is closed. + */ + public void write(byte[] buf, int offset, int size) + throws IOException { + if (isClosed) { + throw new IOException("Stream is closed"); + } + + if (size < 0 || offset < 0 || offset + size > buf.length) { + throw new IndexOutOfBoundsException(); + } + + /* + * Multiple threads blocked on write operation may return results + * interleaved arbitrarily. From an application perspective, the + * results would be indeterministic. So "writer locks" are + * introduced for "write" operation to the same socket. + */ + synchronized (writerLock) { + while (size > 0) { + int res = send0(buf, offset, size); + + if (res <= 0) { + throw new IOException("Data send failed"); + } + + offset += res; + size -= res; + } + } + } + + /* + * Closes this output stream and releases any system resources + * associated with this stream. + * @exception IOException if an I/O error occurs. + */ + public void close() throws IOException { + synchronized (BTSPPConnectionImpl.this) { + if (isClosed) { + return; + } + isClosed = true; + objects--; + if (objects == 0) { + close0(); + } + } + } + } + + /* + * Closes this connection. + * @throws IOException if I/O error. + */ + public void close() throws IOException { + synchronized (this) { + if (isClosed()) { + return; + } + resetRemoteDevice(); + objects--; + if (objects == 0) { + close0(); + } + } + } + + /* + * Closes client connection. + * + * Note: the method gets native connection handle directly from + * handle field of L2CAPConnectionImpl object. + * + * @throws IOException if any I/O error occurs + */ + private native void close0() throws IOException; + + /* + * Reads data from a packet received via Bluetooth stack. + * + * Note: the method gets native connection handle directly from + * handle field of BTSPPConnectionImpl object. + * + * @param buf the buffer to read to + * @param off the start offset in array buf + * at which the data to be written + * @param size the maximum number of bytes to read, + * the rest of the packet is discarded. + * @return total number of bytes read into the buffer, + * 0 indicates end-of-data, + * -1 if there is no data available at this moment + * @throws IOException if an I/O error occurs + */ + protected native int receive0(byte[] buf, int off, int size) + throws IOException; + + /* + * Returns the number of bytes available to be read from the connection + * without blocking. + * + * Note: the method gets native connection handle directly from + * handle field of BTSPPConnectionImpl object. + * + * @return the number of available bytes + * @throws IOException if any I/O error occurs + */ + private native int available0() throws IOException; + + /* + * Sends the specified data via Bluetooth stack. + * + * Note: the method gets native connection handle directly from + * handle field of L2CAPConnectionImpl object. + * + * @param buf the data to send + * @param off the offset into the data buffer + * @param size the size of data in the buffer + * @return total number of send bytes, + * or -1 if nothing is send + * @throws IOException if an I/O error occurs + */ + protected native int send0(byte[] buf, int off, int size) throws IOException; + + + /* Opens client connection. */ + private void doOpen() throws IOException { + /* + * create native connection object + * Note: the method create0 sets resulting native + * connection handle directly to the field handle. + */ + create0(url.authenticate, url.encrypt, url.master); + + byte[] address = BluetoothUtils.getAddressBytes(url.address); + + try { + // establish connection + connect0(address, url.port); + } catch (IOException e) { + throw new BluetoothConnectionException( + BluetoothConnectionException.FAILED_NOINFO, + e.getMessage()); + + } + } + + /* + * Creates a client connection object. + * + * Note: the method gets native connection handle directly from + * handle field of BTSPPConnectionImpl object. + * + * @param auth true if authication is required + * @param enc true indicates + * what connection must be encrypted + * @param master true if client requires to be + * a connection's master + * @throws IOException if any I/O error occurs + */ + private native void create0(boolean auth, boolean enc, boolean master) + throws IOException; + + /* + * Starts client connection establishment. + * + * Note: the method gets native connection handle directly from + * handle field of BTSPPConnectionImpl object. + * + * @param addr bluetooth address of device to connect to + * @param cn Channel number (CN) value + * @throws IOException if any I/O error occurs + */ + private native void connect0(byte[] addr, int cn) throws IOException; + +} diff --git a/java/midp/com/sun/jsr082/bluetooth/btspp/BTSPPNotifierImpl.jpp b/java/midp/com/sun/jsr082/bluetooth/btspp/BTSPPNotifierImpl.jpp new file mode 100644 index 000000000..e8b05e34b --- /dev/null +++ b/java/midp/com/sun/jsr082/bluetooth/btspp/BTSPPNotifierImpl.jpp @@ -0,0 +1,457 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.bluetooth.btspp; + +import javax.bluetooth.*; + +import javax.microedition.io.StreamConnection; +import javax.microedition.io.StreamConnectionNotifier; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InterruptedIOException; + +import java.util.Vector; +import java.util.Enumeration; +/// IMPL_NOTE: revisit +// #ifndef NO_PUSH +import com.sun.jsr082.bluetooth.BluetoothPush; +// #endif +import com.sun.j2me.app.AppPackage; +import com.sun.jsr082.bluetooth.SDDB; +import com.sun.jsr082.bluetooth.BCC; +import com.sun.jsr082.bluetooth.ServiceRecordImpl; +import com.sun.jsr082.bluetooth.BluetoothNotifier; +import com.sun.jsr082.bluetooth.BluetoothUrl; +import com.sun.jsr082.obex.btgoep.BTGOEPNotifier; + +/* + * Bluetooth Serial Port Profile notifier implementation. + */ +public class BTSPPNotifierImpl extends BluetoothNotifier + implements StreamConnectionNotifier { + + /* Static initializer. */ + static { + initialize(); + } + + /* + * Native static class initializer. + */ + private native static void initialize(); + + /* + * Native finalizer. + * Releases all native resources used by this connection. + */ + protected native void finalize(); + + /* + * Identidies this connection at native layer, + * -1 if connection is not open. + */ + private int handle = -1; + + // IMPL_NOTE make private when moving emul below the porting layer completed. + /* + * Temporary stores peer's native handle for accepted connection. + * Used by doAccept method. + */ + int peerHandle = -1; + + /* + * Temporary stores remote device address for accepted connection. + */ + byte[] peerAddress = new byte[6]; + + /* Indicates whether notifier is listening for incoming connections. */ + protected boolean isListenMode = false; + + /* + * Channel Id. + */ + private int cid; + + /* Keeps channel id for service record validation. */ + private DataElement CHANNEL_ID; + + /* Keeps L2CAP UUID for service record validation. */ + static final DataElement L2CAP_UUID = + new DataElement(DataElement.UUID, new UUID(0x0100)); + + /* Keeps RFCOMM UUID for service record validation. */ + static final DataElement RFCOMM_UUID = + new DataElement(DataElement.UUID, new UUID(0x0003)); + + /* Keeps SPP UUID for service record validation. */ + static final DataElement SPP_UUID = + new DataElement(DataElement.UUID, new UUID(0x1101)); + + /* Bluetooth PushRegistry handle, used in native methods only. */ + private int pushHandle = 0; + + /* + * Creates instance of BTSPPNotifierImpl. + * + * @param url BluetoothUrl that represents server + * connection string to create notifier for. + * @param mode I/O access mode + * @throws IOException if there is no available channels to open connection + * @throws ServiceRegistrationException if there is no available channel + */ + public BTSPPNotifierImpl(BluetoothUrl url, int mode) throws IOException, + ServiceRegistrationException { + super(url, mode); + + AppPackage app = AppPackage.getInstance(); + String connUrl = "btspp:" + url.caseSensitiveUrl; +// #ifndef NO_PUSH + if (url.protocol == url.OBEX) { + connUrl = "btgoep:" + url.caseSensitiveUrl; + } + if (app != null && pushCheckout(connUrl, app.getId())) { + serviceRec = BluetoothPush.getServiceRecord(this, connUrl); + checkServiceRecord(); + } else { + serviceRec = createServiceRecord(this, url, doCreate(url)); + } +// #else + serviceRec = createServiceRecord(this, url, doCreate(url)); +// #endif + } + + /* + * Checks out (takes ownership of) an active server connection maintained + * by push subsystem. + * + * @param url URL used during registration of the push entry + * @param suiteId suite id + * @return true if the operation succeeds, false otherwise + */ +// #ifndef NO_PUSH + private native boolean pushCheckout(String url, int suiteId); +// #endif + + /* + * Creates an empty service record for the given URL and channel value. + * + * @param notifier SPP notifier object to be associated with the record + * @param url URL from which a new record is constructed + * @param cn channel value assigned to the notifier + * @return a new service record instance + */ + public static ServiceRecordImpl createServiceRecord( + BTSPPNotifierImpl notifier, BluetoothUrl url, int cn) { + DataElement serviceList = new DataElement(DataElement.DATSEQ); + serviceList.addElement(new DataElement(DataElement.UUID, + new UUID(url.uuid, false))); + serviceList.addElement(SPP_UUID); + DataElement protocolList = new DataElement(DataElement.DATSEQ); + DataElement protocol = new DataElement(DataElement.DATSEQ); + protocol.addElement(L2CAP_UUID); + protocolList.addElement(protocol); + protocol = new DataElement(DataElement.DATSEQ); + protocol.addElement(RFCOMM_UUID); + if (notifier != null) { + notifier.CHANNEL_ID = new DataElement(DataElement.U_INT_1, cn); + protocol.addElement(notifier.CHANNEL_ID); + } else { + protocol.addElement(new DataElement(DataElement.U_INT_1, cn)); + } + protocolList.addElement(protocol); + if (url.protocol == url.OBEX) { + DataElement p = new DataElement(DataElement.DATSEQ); + p.addElement(BTGOEPNotifier.DE_OBEX_UUID); // obex UUID + protocolList.addElement(p); + } + int[] attrIDs; + DataElement[] attrVals; + if (url.name != null) { + DataElement name = new DataElement(DataElement.STRING, url.name); + attrIDs = new int[] { + ServiceRecordImpl.SERVICE_CLASS_ATTR_ID, + ServiceRecordImpl.PROTOCOL_DESCRIPTOR_LIST, + ServiceRecordImpl.NAME_ATTR_ID + }; + attrVals = new DataElement[] { serviceList, protocolList, name }; + } else { + attrIDs = new int[] { + ServiceRecordImpl.SERVICE_CLASS_ATTR_ID, + ServiceRecordImpl.PROTOCOL_DESCRIPTOR_LIST + }; + attrVals = new DataElement[] { serviceList, protocolList }; + } + return new ServiceRecordImpl(notifier, attrIDs, attrVals); + } + + /* + * Creates an empty service record for the given URL + * + * @param url URL from which a new record is constructed + * @return a new service record instance + */ + public static ServiceRecordImpl createServiceRecord(String url) { + return createServiceRecord(null, new BluetoothUrl(url), 0); + } + + /* + * Ensures that the service record is valid. + * + * @throws ServiceRegistrationException in case described in the + * JSR specification + */ + protected void checkServiceRecord() throws ServiceRegistrationException { + synchronized (serviceRec) { + // check if the ServiceClassIDList is not missing + if (serviceRec.getAttributeValue( + ServiceRecordImpl.SERVICE_CLASS_ATTR_ID) == null) { + throw new ServiceRegistrationException( + "ServiceClassIDList is missing."); + } + // check the ProtocolList is not missed + DataElement protocolList = serviceRec.getAttributeValue( + ServiceRecordImpl.PROTOCOL_DESCRIPTOR_LIST); + if (protocolList == null) { + throw new ServiceRegistrationException( + "ProtocolDescriptorList is missing."); + } + Enumeration protocolListEnum = + (Enumeration)protocolList.getValue(); + boolean l2cap = false; + boolean rfcomm = false; + while (protocolListEnum.hasMoreElements()) { + Enumeration protocolEnum = (Enumeration)((DataElement) + protocolListEnum.nextElement()).getValue(); + // check that L2CAP/RFCOMM UUIDs are not missing, check the CN + // is not missing and the value has not changed + while (protocolEnum.hasMoreElements()) { + DataElement element = (DataElement)protocolEnum. + nextElement(); + if (compareDataElements(element, L2CAP_UUID)) { + l2cap = true; + } + if (compareDataElements(element, RFCOMM_UUID)) { + rfcomm = true; + if (CHANNEL_ID != null) { + if (!protocolEnum.hasMoreElements() || + !compareDataElements((DataElement)protocolEnum. + nextElement(), CHANNEL_ID)) { + throw new ServiceRegistrationException( + "Channel value has changed."); + } + } + } + if (l2cap && rfcomm) { + return; + } + } + } + } + throw new ServiceRegistrationException("L2CAP UUID is missing."); + } + + /* + * Ensures that this notifier can accept connections. + * + * @throws IOException if notifier is closed or device is not + * in connectable mode + * @throws ServiceRegistrationException if the service record is not valid + */ + private void ensureConnectable() throws IOException { + if (isClosed) { + throw new BluetoothConnectionException( + BluetoothConnectionException.FAILED_NOINFO, + "Notifier is closed."); + } + if (!BCC.getInstance().isConnectable()) { + throw new BluetoothStateException("The device is not connectable."); + } + checkServiceRecord(); + } + + /* + * Accepts client connection to the service this notifier is assigned to. + * Adds corresponding service record to the SDDB, blocks until a successfull + * connection to a client is established, retrieves the connection. + * + * @return bi-directional connection to a client just accepted. + * + * @throws IOException if notifier is closed or device is not + * in connectable mode. + */ + public StreamConnection acceptAndOpen() + throws IOException, ServiceRegistrationException { + ensureConnectable(); + + // switch on listen mode if it has not been done yet + doListen(); + + // adds the record only if is not yet in SDDB + SDDB.getInstance().updateServiceRecord(serviceRec); + + StreamConnection client; + do { + ensureConnectable(); + + // accept incoming connections if any + client = doAccept(); + } while (client == null); + + return client; + } + + /* + * Closes this notifier making corresponding service record inaccessible. + * updates the information on the communication server. + * + * @throws IOException if an error occured lower in Bluetooth stack. + */ + public void close() throws IOException { + if (isClosed) { + return; + } + isClosed = true; + + SDDB.getInstance().removeServiceRecord(serviceRec); + + doClose(); + } + + /* + * Force listen mode by calling underlying stack methods. + * + * @throws IOException if an error occured + */ + + private void doListen() throws IOException { + // force listening if it had not been done yet + if (!isListenMode) { + listen0(); + + isListenMode = true; + } + } + + /* + * Force Bluetooth stack to listen for incoming client connections. + * + * Note: the method gets native connection handle directly from + * handle field of BTSPPNotifierImpl object. + * + * @throws IOException if an I/O error occurs + */ + private native void listen0() throws IOException; + + /* + * Advertises acception by calling underlying stack methods. + * + * @return BTSPPConnection instance to work with accepted client + */ + private StreamConnection doAccept() throws IOException { + if (!isListenMode) { + throw new BluetoothStateException("Device is not in listen mode"); + } + + /* + * Note: native handle is set to peerHandleID field directly + * by accept0 method and retrieved by L2CAPConnectionImpl constructor. + */ + accept0(); + + return new BTSPPConnectionImpl(url, mode, this); + } + + /* + * Accepts incoming client connection request. + * + * Note: the method gets native connection handle directly from + * handle field of BTSPPNotifierImpl object. + * + * Note: new native connection handle to work with accepted incoming + * client connection is setted directly to handle field of + * appropriate BTSPPConnectionImpl object. + * + * @return 0 if incoming client connection was successfully + * accepted; + * -1 if there is no pending incoming connections + * @throws IOException if an I/O error occurs + */ + protected native int accept0() throws IOException; + + + /* + * Closes this notifier at native layer. + */ + private void doClose() throws IOException { + close0(); + } + + /* + * Closes this server connection. + * Releases all native resources (such as sockets) owned by this notifier. + * + * Note: the method gets native connection handle directly from + * handle field of BTSPPNotifierImpl object. + * + * @throws IOException IOException if an I/O error occurs + */ + private native void close0() throws IOException; + + + /* + * Creates an instanse of server connection at native layer. + * + * @param url BluetoothUrl that represents server + * connection string to create notifier for + * @return selected channel number to listen for incoming connections + */ + private int doCreate(BluetoothUrl url) throws IOException { + return create0(url.authenticate, url.authorize, + url.encrypt, url.master); + } + + /* + * Creates a server connection. + * + * Note: the method sets native connection handle directly to + * handle field of BTSPPNotifierImpl object. + * + * @param auth true if authication is required + * @param authz true if authorization is required + * @param enc true indicates + * what connection must be encrypted + * @param master true if client requires to be + * a connection's master + * @return selected channel number to listen for incoming connections on + * @throws IOException IOException if an I/O error occurs + */ + private native int create0(boolean auth, boolean authz, + boolean enc, boolean master) throws IOException; + +} diff --git a/java/midp/com/sun/jsr082/obex/HeaderSetImpl.java b/java/midp/com/sun/jsr082/obex/HeaderSetImpl.java new file mode 100644 index 000000000..82f314744 --- /dev/null +++ b/java/midp/com/sun/jsr082/obex/HeaderSetImpl.java @@ -0,0 +1,288 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.obex; + +import javax.obex.HeaderSet; +import java.io.IOException; +import java.util.Hashtable; +import java.util.Enumeration; +import java.util.Calendar; +import java.util.Vector; + +public class HeaderSetImpl implements HeaderSet { + + /* Debug information, should be false for RR. */ + private static final boolean DEBUG = false; + + public static final int OWNER_SERVER = 1; + public static final int OWNER_CLIENT = 2; + public static final int OWNER_SERVER_USER = 3; + public static final int OWNER_CLIENT_USER = 4; + + static final int TYPE_UNICODE = 0; + static final int TYPE_BYTEARRAY = 1; + static final int TYPE_BYTE = 2; + static final int TYPE_LONG = 3; + static final int TYPE_SPECIAL_TIME_4 = 4; + static final int TYPE_SPECIAL_TIME_ISO = 5; + static final int TYPE_SPECIAL_TYPE = 6; + static final int TYPE_UNSUPPORTED = 7; + static final int TYPE_AUTH_CHALLENGE = 8; + static final int TYPE_AUTH_RESPONSE = 9; + + /* package protected */ int owner; + /* package protected */ int packetType; + + private Hashtable headers; + Vector challenges = new Vector(); + + public HeaderSetImpl(int owner) { + this.owner = owner; + headers = new Hashtable(5); + } + + + /* + * Adds headers from h to this HeaderSet. + */ + void merge(HeaderSetImpl h) { + int[] idList = h.getHeaderList(); + + if (idList == null) { + return; + } + + for (int i = 0; i < idList.length; i++) { + int id = idList[i]; + Object val = h.getHeader(id); + int type = internalType(id); + switch (type) { + case TYPE_UNICODE: + case TYPE_BYTE: + case TYPE_LONG: + case TYPE_SPECIAL_TYPE: + setHeader(id, val); + break; + + case TYPE_BYTEARRAY: + byte[] array = (byte[]) val; + byte[] copy = new byte[array.length]; + System.arraycopy(array, 0, copy, 0, array.length); + setHeader(id, (Object) copy); + break; + + case TYPE_SPECIAL_TIME_4: + case TYPE_SPECIAL_TIME_ISO: + Calendar cal = (Calendar) val; + Calendar calCopy = Calendar.getInstance(); + calCopy.setTime(cal.getTime()); + setHeader(id, (Object) calCopy); + break; + + default: + // no default + if (DEBUG) { + System.out.println("Debug: unknown header id"); + } + } + } + } + + + /* + * Makes private copy of the headers. + */ + HeaderSetImpl(HeaderSetImpl h) { + this.packetType = h.packetType; + this.owner = h.owner; + headers = new Hashtable(5); + merge(h); + } + + boolean isSendable() { + return owner >= OWNER_SERVER_USER; + } + + // interface functions: + public void createAuthenticationChallenge(java.lang.String realm, + boolean userID, boolean access) { + + ObexAuth auth = ObexAuth.createChallenge(realm, userID, access); + challenges.addElement(auth); + } + + public Object getHeader(int headerID) { + Object value = headers.get(new Integer(headerID)); + + if (value != null) { + return value; + } + + // check validness of headerID + int type = internalType(headerID); + + if (type >= TYPE_UNSUPPORTED) { + throw new IllegalArgumentException("bad header id"); + } + return null; + } + + public int[] getHeaderList() { + synchronized (headers) { + if (headers.isEmpty()) { + return null; + } + Enumeration keys = headers.keys(); + int[] ids = new int[headers.size()]; + int i = 0; + + while (keys.hasMoreElements()) { + Object obj = keys.nextElement(); + ids[i++] = ((Integer)obj).intValue(); + } + return ids; + } + } + + public int getResponseCode() throws IOException { + if (owner != OWNER_CLIENT) { + throw new IOException("invalid use"); + } + if (packetType == ObexPacketStream.OPCODE_CONTINUE) { + throw new IOException("operation not finished"); + } + return packetType; + } + + public void setHeader(int headerID, Object headerValue) { + int type = internalType(headerID); + String errormsg = "incompatible header object"; + + if (type >= TYPE_UNSUPPORTED) { + throw new IllegalArgumentException("bad header id"); + } + + if (headerValue == null) { + synchronized (headers) { + headers.remove(new Integer(headerID)); + } + return; + } + + boolean fail = false; + switch (type) { + case TYPE_UNICODE: + if (!(headerValue instanceof String)) fail = true; + break; + + case TYPE_BYTEARRAY: + if (!(headerValue instanceof byte[])) fail = true; + break; + + case TYPE_BYTE: + if (!(headerValue instanceof Byte)) fail = true; + break; + + case TYPE_LONG: + if (!(headerValue instanceof Long)) { + fail = true; + break; + } + long value = ((Long)headerValue).longValue(); + + if (value < 0L || value > 0xFFFFFFFFL) { + errormsg = "long value out of range 0 .. 0xFFFFFFFF"; + fail = true; + } + break; + + case TYPE_SPECIAL_TIME_4: + if (!(headerValue instanceof Calendar)) fail = true; + break; + + case TYPE_SPECIAL_TIME_ISO: + if (!(headerValue instanceof Calendar)) fail = true; + break; + + case TYPE_SPECIAL_TYPE: + if (!(headerValue instanceof String)) fail = true; + break; + // no default + } + + if (fail) { + throw new IllegalArgumentException(errormsg); + } + synchronized (headers) { + headers.put(new Integer(headerID), headerValue); + } + } + + static final int internalType(int headerID) { + if ((headerID & ~0xFF) != 0) { + return TYPE_UNSUPPORTED; + } + if ((headerID & 0x30) == 0x30) { + // user defined + return headerID >> 6; + } + + switch (headerID) { + case HeaderSet.TIME_ISO_8601: + return TYPE_SPECIAL_TIME_ISO; + + case HeaderSet.TIME_4_BYTE: + return TYPE_SPECIAL_TIME_4; + + case HeaderSet.TYPE: + return TYPE_SPECIAL_TYPE; + + case HeaderSet.COUNT: + case HeaderSet.LENGTH: + return TYPE_LONG; + + case HeaderSet.NAME: + case HeaderSet.DESCRIPTION: + return TYPE_UNICODE; + + case HeaderSet.TARGET: + case HeaderSet.HTTP: + case HeaderSet.WHO: + case HeaderSet.OBJECT_CLASS: + case HeaderSet.APPLICATION_PARAMETER: + return TYPE_BYTEARRAY; + + case ObexPacketStream.HEADER_AUTH_CHALLENGE: + return TYPE_AUTH_CHALLENGE; + + case ObexPacketStream.HEADER_AUTH_RESPONSE: + return TYPE_AUTH_RESPONSE; + + default: + return TYPE_UNSUPPORTED; + } + } +} diff --git a/java/midp/com/sun/jsr082/obex/ObexAuth.java b/java/midp/com/sun/jsr082/obex/ObexAuth.java new file mode 100644 index 000000000..39e0445a3 --- /dev/null +++ b/java/midp/com/sun/jsr082/obex/ObexAuth.java @@ -0,0 +1,515 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.obex; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Vector; +import javax.obex.Authenticator; +import javax.obex.PasswordAuthentication; + +/* + * Obex protocol authentication functinality. + */ +class ObexAuth { + + /* Debug information, should be false for RR. */ + private static final boolean DEBUG = false; + + private static byte[] column = { (byte)':' }; + + String realm; + boolean userID; + boolean access; + byte[] nonce; + private static int counter = 0; + + // used in prepareChallenge, addChallenge + private byte[] realm_array; + private int challengeLength; + + static ObexAuth createChallenge(String realm, + boolean userID, boolean access) { + return new ObexAuth(realm, null, userID, access); + } + + private ObexAuth(String realm, byte[] nonce, boolean userID, + boolean access) { + this.realm = realm; + this.nonce = nonce; + this.userID = userID; + this.access = access; + } + + /* + * Prepare challenge before adding to packet. + * @return length of challenge + */ + int prepareChallenge() { + if (challengeLength != 0) { + return challengeLength; + } + try { + int len = 24; + realm_array = null; + if (realm != null) { + realm_array = realm.getBytes("UTF-16BE"); + len += 3 + realm_array.length; + } + challengeLength = len; + } catch (UnsupportedEncodingException e) { + if (DEBUG) { + System.out.println("prepareChallenge(): ERROR, no encoding"); + } + return 0; + } + return challengeLength; + } + + int addChallenge(byte[] packet, int offset) throws IOException { + int len = prepareChallenge(); + + packet[offset] = (byte)ObexPacketStream.HEADER_AUTH_CHALLENGE; + packet[offset+1] = (byte) (len >> 8); + packet[offset+2] = (byte) (len & 255); + packet[offset+3] = (byte) 0; // nonce tag (0x0) + packet[offset+4] = (byte) 16; // nonce len (16) (1 bytes) + nonce = makeNonce(); // 16 byte of nonce + if (DEBUG) { + print("addChallenge: nonce", nonce); + } + System.arraycopy(nonce, 0, packet, offset + 5, 16); + packet[offset+21] = (byte) 1; // options tag (0x1) + packet[offset+22] = (byte) 1; // options length (1) + packet[offset+23] = (byte) ((userID ? 1 : 0) + (access ? 0 : 2)); + if (realm != null) { + int realm_len = realm_array.length; + packet[offset+24] = (byte) 2; // realm tag (0x2) + + // realm length including encoding (1 byte) + packet[offset+25] = (byte) (realm_len + 1); + packet[offset+26] = (byte) 0xFF; // realm encoding UNICODE + System.arraycopy(realm_array, 0, packet, offset+27, realm_len); + } + return len; + } + + private static byte[] makeNonce() throws IOException { + SSLWrapper md5 = new SSLWrapper(); + byte[] timestamp = createTimestamp(); + md5.update(timestamp, 0, timestamp.length); + md5.update(column, 0, 1); + byte[] privateKey = getPrivateKey(); + byte[] nonce = new byte[16]; + md5.doFinal(privateKey, 0, privateKey.length, nonce, 0); + return nonce; + } + + /* + * Creates timestamp. + * No strict specification for timestamp generation in OBEX 1.2 + * @return timestamp value + */ + private static byte[] createTimestamp() { + long time = System.currentTimeMillis(); + byte[] timestamp = new byte[9]; + timestamp[0] = (byte)(time >> 56); + timestamp[1] = (byte)(time >> 48); + timestamp[2] = (byte)(time >> 40); + timestamp[3] = (byte)(time >> 32); + timestamp[4] = (byte)(time >> 24); + timestamp[5] = (byte)(time >> 16); + timestamp[6] = (byte)(time >> 8); + timestamp[7] = (byte)(time); + + synchronized (ObexAuth.class) { + timestamp[8] = (byte)(counter++); + } + return timestamp; + } + + private static byte[] privateKey = null; + + /* + * Create and return private key. + * Weak security scheme. Should be rewritten for more secure + * implementation if used outside emulator. + */ + private synchronized static byte[] getPrivateKey() throws IOException { + if (privateKey != null) { + return privateKey; + } + SSLWrapper md5 = new SSLWrapper(); + byte[] keyData = null; + + try { + keyData = "timestamp = ".getBytes("ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + md5.update(keyData, 0, keyData.length); + byte[] timestamp = createTimestamp(); + privateKey = new byte[16]; + md5.doFinal(timestamp, 0, timestamp.length, privateKey, 0); + return privateKey; + } + + static void makeDigest(byte[] buffer, int offset, byte[] nonce, + byte[] password) + throws IOException { + SSLWrapper md5 = new SSLWrapper(); + md5.update(nonce, 0, 16); + md5.update(column, 0, 1); + md5.doFinal(password, 0, password.length, buffer, offset); + } + + static ObexAuth parseAuthChallenge(byte[] buffer, int packetOffset, + int length) + throws IOException { + if (DEBUG) { + System.out.println("ObexAuth.parseAuthChallenge()"); + } + + // default values + boolean readonly = false; + boolean needUserid = false; + byte[] nonce = null; + String realm = null; + + // skiping header type and length + int offset = packetOffset + 3; + length += packetOffset; + + // decoding data in buffer + while (offset < length) { + int tag = buffer[offset] & 0xFF; + int len = buffer[offset + 1] & 0xFF; + offset += 2; + + + switch (tag) { + case 0x0: // nonce + if (len != 16 || nonce != null) { + throw new IOException("protocol error"); + } + nonce = new byte[16]; + System.arraycopy(buffer, offset, nonce, 0, 16); + if (DEBUG) { + print("got challenge: nonce", nonce); + } + break; + case 0x1: // options + if (len != 1) { + throw new IOException("protocol error"); + } + int options = buffer[offset]; + readonly = ((options & 2) != 0); + needUserid = ((options & 1) != 0); + break; + case 0x2: // realm + try { + int encodingID = buffer[offset] & 0xFF; + String encoding = null; + if (encodingID == 255) encoding = "UTF-16BE"; + else if (encodingID == 0) encoding = "US-ASCII"; + else if (encodingID < 10) encoding = "ISO-8859-" + + encoding; + else throw new UnsupportedEncodingException(); + + realm = new String(buffer, offset + 1, + len - 1, encoding); + } catch (UnsupportedEncodingException e) { + // already: realm = null; + } + } + offset += len; + } + if (offset != length) { + throw new IOException("protocol error"); + } + return new ObexAuth(realm, nonce, needUserid, !readonly); + } + + int replyAuthChallenge(byte[] buffer, int packetOffset, + Authenticator authenticator) throws IOException { + + if (DEBUG) { + System.out.println("ObexAuth.replyAuthChallenge()"); + } + + if (realm == null) { + realm = ""; + } + + byte[] password = null; + byte[] username = null; + + try { + PasswordAuthentication pass = + authenticator.onAuthenticationChallenge(realm, userID, access); + + password = pass.getPassword(); + int uidLen = 0; + // userid subheader length with subheader and + + username = pass.getUserName(); + if (userID || username != null) { + + // username is required but not provided + if (userID && username.length == 0) { + if (DEBUG) { + System.out.println("ObexAuth.replyAuthChallenge():" + + " required username not provided"); + } + throw new Exception(); + } + uidLen = 2 + username.length; + + // maximum supported username length = 20 + if (uidLen > 22) uidLen = 22; + } + + int len = 39 + uidLen; + // byte[] response = new byte[len]; + buffer[packetOffset + 0] = (byte)ObexPacketStream + .HEADER_AUTH_RESPONSE; + buffer[packetOffset + 1] = (byte) (len >> 8); + buffer[packetOffset + 2] = (byte) (len & 255); + buffer[packetOffset + 3] = 0x0; // tag (Request-Digest) + buffer[packetOffset + 4] = 16; // digest len (16) + makeDigest(buffer, packetOffset + 5, nonce, password); + buffer[packetOffset + 21] = 0x02; // tag nonce + buffer[packetOffset + 22] = 16; // nonce len (16) + System.arraycopy(nonce, 0, buffer, packetOffset + 23, 16); + if (DEBUG) { + print("send response: nonce", nonce); + } + if (uidLen > 2) { + buffer[packetOffset + 39] = 0x01; // tag userid + buffer[packetOffset + 40] = (byte) (uidLen - 2); // userid len + System.arraycopy(username, 0, buffer, packetOffset + 41, + uidLen - 2); + } + + if (DEBUG) { + System.out.println("ObexAuth.replyAuthChallenge():" + + " response generated"); + } + return len; + + // need to create authentication response + // we should resend previous packet with the authentication response + + } catch (Throwable t) { + if (DEBUG) { + System.out.println("ObexAuth.replyAuthChallenge(): exception"); + t.printStackTrace(); + } + // will caught NullPointerException if authenticator + // was not provided + // will caught exceptions in handler + // will caught NullPointerException exception + // if username == null and needUserid + // + // wrong response from client application, + // ignoring authentication challenge + // client will receive UNAUTHORIZED + } + return 0; + } + + private static void print(String msg, byte[] array) { + if (DEBUG) { + System.out.println(msg); + if (array == null) { + System.out.println("[0] = NULL"); + return; + } + System.out.print("[" + array.length + "]"); + for (int i = 0; i < array.length; i++) { + System.out.print(" " + Integer.toHexString(array[i] & 0xFF)); + } + System.out.println(""); + } + } + + private static boolean compare(byte[] src1, byte[] src2) { + for (int i = 0; i < 16; i++) { + if (src1[i] != src2[i]) + return false; + } + return true; + } + + static boolean checkAuthResponse(byte[] buffer, int packetOffset, + int length, ObexPacketStream stream, Vector challenges) + throws IOException { + + if (DEBUG) { + System.out.println("ObexAuth.parseAuthResponse()"); + } + + // skiping header type and length + int offset = packetOffset + 3; + length += packetOffset; + + byte[] digest = null; + byte[] username = null; + byte[] nonce = null; + + // decoding data in buffer + while (offset < length) { + int tag = buffer[offset] & 0xFF; + int len = buffer[offset + 1] & 0xFF; + offset += 2; + + switch (tag) { + case 0x0: // digest + if (DEBUG) { + System.out.println("got digest"); + } + if (len != 16 || digest != null) { + throw new IOException("protocol error (1)"); + } + digest = new byte[16]; + System.arraycopy(buffer, offset, digest, 0, 16); + if (DEBUG) { + print("got response: digest", digest); + } + break; + case 0x1: // username + if (DEBUG) { + System.out.println("got username"); + } + if (len > 20 || len == 0 || username != null) { + throw new IOException("protocol error (2)"); + } + username = new byte[len]; + System.arraycopy(buffer, offset, username, 0, len); + break; + case 0x2: // nonce + if (DEBUG) { + System.out.println("got nonce"); + } + if (len != 16 || nonce != null) { + throw new IOException("protocol error (3)"); + } + nonce = new byte[16]; + System.arraycopy(buffer, offset, nonce, 0, 16); + if (DEBUG) { + print("got response: nonce", nonce); + } + break; + default: + if (DEBUG) { + System.out.println("unknown tag = " + tag); + } + } + offset += len; + } + + if (offset != length) { + throw new IOException("protocol error (4)"); + } + + + // check nonce and select auth object + ObexAuth auth = null; + + if (nonce == null) { + if (DEBUG) { + System.out.println("no nonce received, using first auth"); + } + if (challenges.size() == 0) { + return false; + } + auth = (ObexAuth) challenges.elementAt(0); + nonce = auth.nonce; + challenges.removeElementAt(0); + } else { + if (DEBUG) { + System.out.println("nonce provided, searching for auth"); + System.out.println("challenges = " + challenges.size()); + } + for (int i = 0; i < challenges.size(); i++) { + ObexAuth a = (ObexAuth) challenges.elementAt(i); + if (compare(nonce, a.nonce)) { + if (DEBUG) { + System.out.println("nonce is in " + i + " challenge"); + } + auth = a; + challenges.removeElementAt(i); + break; + } + } + if (DEBUG) { + System.out.println("auth = " + auth); + } + if (auth == null) + return false; + } + + // check username existance + if (auth.userID && username == null) { + if (DEBUG) { + System.out.println("need username!"); + } + // NOTE: may be too strict + stream.onAuthenticationFailure(username); + return false; + } + + // ask password from authenticator and check digest + try { + if (DEBUG) { + System.out.println("running onAuthenticationResponse()..."); + } + byte[] password = + stream.authenticator.onAuthenticationResponse(username); + byte[] localDigest = new byte[16]; + makeDigest(localDigest, 0, nonce, password); + if (DEBUG) { + System.out.println("digest created"); + } + boolean res = compare(localDigest, digest); + + if (res == false) { + if (DEBUG) { + System.out.println("Calling onAuthenticationFailure().."); + } + stream.onAuthenticationFailure(username); + } + return res; + } catch (Throwable t) { + // catch localDigest = null, crypto and user code exception + if (DEBUG) { + System.out.println("exception"); + } + stream.onAuthenticationFailure(username); + return false; + } + } +} diff --git a/java/midp/com/sun/jsr082/obex/ObexPacketStream.java b/java/midp/com/sun/jsr082/obex/ObexPacketStream.java new file mode 100644 index 000000000..60a656e8c --- /dev/null +++ b/java/midp/com/sun/jsr082/obex/ObexPacketStream.java @@ -0,0 +1,922 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.obex; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; +import java.util.Vector; +import java.util.Stack; +import javax.microedition.io.Connection; +import javax.obex.ResponseCodes; +import javax.obex.Authenticator; +import com.sun.j2me.log.Logging; + +/* + * Obex core protocol. + */ +public abstract class ObexPacketStream implements Connection { + + /* Debug information, should be false for RR. */ + private static final boolean DEBUG = false; + private static final boolean DEBUG2 = false; + + // OBEX operations opcodes + static final int OPCODE_CONNECT = 0x80; + static final int OPCODE_DISCONNECT = 0x81; + static final int OPCODE_PUT = 0x02; + static final int OPCODE_GET = 0x03; + static final int OPCODE_SETPATH = 0x85; + static final int OPCODE_CONTINUE = 0x90; + static final int OPCODE_ABORT = 0xFF; + static final int OPCODE_FINAL = 0x80; + static final int OPCODE_GET_FINAL = OPCODE_GET | OPCODE_FINAL; + + static final byte[] PACKET_ABORT = { (byte) OPCODE_ABORT, 0, 0 }; + static final byte[] PACKET_CONTINUE = { (byte) OPCODE_CONTINUE, 0, 0}; + static final byte[] PACKET_DISCONNECT = { + (byte) OPCODE_DISCONNECT, 0, 0}; + static final byte[] PACKET_SUCCESS = { + (byte) ResponseCodes.OBEX_HTTP_OK, 0, 0}; + + static final byte[] PACKET_BAD_REQUEST = { + (byte) ResponseCodes.OBEX_HTTP_BAD_REQUEST, 0, 0}; + + static final byte[] PACKET_NOT_IMPLEMENTED = { + (byte) ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED, 0, 0}; + + private static final int HEADER_BODY = 0x48; + private static final int HEADER_EOFBODY = 0x49; + private static final int HEADER_CONNECTION_ID = 0xCB; + static final int HEADER_AUTH_CHALLENGE = 0x4D; + static final int HEADER_AUTH_RESPONSE = 0x4E; + + static TimeZone utcTimeZone = TimeZone.getTimeZone("UTC"); + + private static final int TRANSPORT_READ_INTERVAL = 10; + + private ObexTransport transport; + + Authenticator authenticator; + + /* + * Generated authentication responses. They will be send in sendPacket(). + * Stored in byte[] format. + */ + Vector authResponses = new Vector(); + + /* + * Sent authentication challenges. + * They will be used for check authentication responses. + */ + Vector authChallenges = new Vector(); + + /* + * True when buffer contains packet for sending and some headers can + * be added. + */ + boolean moreHeaders = false; + + + /* + * Outgoing packet contains authentication challenges, so it should be + * sent immediatly. + */ + boolean challengesToSend = false; + + /* + * True if one of sending headers cannot feet in empty packet. + */ + boolean headerOverflow = false; + + /* + * True if sending packet contains target header. + */ + boolean containsTargetHeader = false; + + /* + * Queue of outgoing headers, not feet in packet. + */ + Vector queuedHeaders; + QueuedHeader newHeader; + Stack emptyHeadersPool; + + /* + * Set when sending auth challenge, + * reset when received valid auth response in next + * packet. + */ + boolean authFailed = false; + + /* + * True if this is ClientSession, false in ServerConnectionImpl. + */ + boolean isClient; + + /* + * Client is connected flag. Ignored by ServerConnectionImpl. + */ + boolean isConnected; + + int OBEX_MAXIMUM_PACKET_LENGTH; + + byte[] buffer, cache; + int packetLength; + int packetOffset; + int packetType; + + int maxSendLength; + + boolean dataOpened, dataClosed; + boolean isEof; + int dataOffset; + + /* + * Connection id used in setConnectionID, + * getConnectioID. + */ + + ObexPacketStream(ObexTransport transport) { + this.transport = transport; + OBEX_MAXIMUM_PACKET_LENGTH = transport.getMaximumPacketSize(); + buffer = new byte[OBEX_MAXIMUM_PACKET_LENGTH]; + cache = new byte[OBEX_MAXIMUM_PACKET_LENGTH]; + maxSendLength = OBEX_MAXIMUM_PACKET_LENGTH; + newHeader = new QueuedHeader(this); + queuedHeaders = new Vector(); + emptyHeadersPool = new Stack(); + } + + + // interface visible function + public void close() { + try { + if (transport != null) { + transport.close(); + } + } catch (IOException e) { + // nothing + } + transport = null; + } + + public void setAuthenticator(Authenticator authenticator) { + if (authenticator == null) { + throw new NullPointerException("null authenticator"); + } + this.authenticator = authenticator; + } + + public Connection getTransport() + throws IOException { + if (transport == null) { + throw new IOException("connection error"); + } + return transport.getUnderlyingConnection(); + } + + /* + * Sets link broken flag. + */ + void brokenLink() { + close(); + } + + boolean isClosed() { + return transport == null; + } + + void packetBegin(byte[] head) { + if (DEBUG) { + System.out.println("packetBegin()"); + } + + containsTargetHeader = false; + moreHeaders = true; + challengesToSend = false; + System.arraycopy(head, 0, buffer, 0, head.length); + packetLength = head.length; + authChallenges.removeAllElements(); + dataOpened = false; + dataClosed = false; + dataOffset = -3; // generate aoobe when accessed + } + + int packetAddData(byte[] data, int offset, int length) { + if (DEBUG) { + System.out.println("packetAddData()"); + } + // preventing writing several data blocks, just in case + if (dataClosed) + return 0; + + if (!dataOpened) { + // let it be at least 3 bytes workload to create new Body header + if (packetLength + 6 > maxSendLength) { + return 0; + } + buffer[packetLength] = HEADER_BODY; + dataOffset = packetLength; + packetLength += 3; + dataOpened = true; + } + + int len; + if (packetLength + length > maxSendLength) { + len = maxSendLength - packetLength; + } else { + len = length; + } + System.arraycopy(data, offset, buffer, packetLength, len); + packetLength += len; + return len; + } + + int getPacketLength() { + return packetLength; + } + + void restorePacketLength(int len) { + packetLength = len; + } + + boolean packetEOFBody() { + if (DEBUG) { + System.out.println("packetEOFBody()"); + } + if (dataClosed) { + return false; + } + if (dataOpened) { + buffer[dataOffset+0] = HEADER_EOFBODY; + return true; + } else { + if (packetLength + 3 > maxSendLength) { + return false; + } + buffer[packetLength++] = HEADER_EOFBODY; + buffer[packetLength++] = 0; // length + buffer[packetLength++] = 3; + return true; + } + } + + void packetMarkFinal() { + if (DEBUG) { + System.out.println("packetMarkFinal()"); + } + buffer[0] |= 0x80; + } + + void setPacketType(int type) { + if (DEBUG) { + System.out.println("setPacketType()"); + } + buffer[0] = (byte) type; + } + + /* + * Finish packet and send it. Remove Connection ID header if packet also + * contains TARGET header. + */ + void packetEndStripConnID() throws IOException { + + // first header id is in 3 byte on all packet except CONNECT + // and this function is known not to be called for connect() operation. + if ((buffer[3] & 0xFF) == HeaderSetImpl.TARGET) { + packetLength -= 5; + + // length of Connection ID packet is 5 bytes: + // 1 byte header + 4 byte int value + for (int i = 3; i < packetLength; i++) { + buffer[i] = buffer[i + 5]; + } + } + packetEnd(); + } + + void packetEnd() throws IOException { + if (DEBUG) { + System.out.println("packetEnd()"); + } + moreHeaders = false; + + if (transport == null) { + throw new IOException("connection error"); + } + + if (dataOpened) { + // closing Body header + int len = packetLength - dataOffset; + buffer[dataOffset+1] = (byte)(len >> 8); + buffer[dataOffset+2] = (byte)len; + dataOpened = false; + dataClosed = true; + } + // update packet length field + buffer[1] = (byte)(packetLength / 0x100); + buffer[2] = (byte)(packetLength % 0x100); + + if (DEBUG) { + int len = packetLength; + if (!DEBUG2 && len > 20) { + len = 20; + } + System.out.println("send:"); + for (int i = 0; i < len; i++) { + System.out.print(" 0x" + Integer.toHexString(buffer[i] & 0xFF)); + int chr = buffer[i] & 0xFF; + if (chr >= 32 && chr < 128) { + System.out.print("(" + (char)(buffer[i] & 0xFF) + ")"); + } + } + if (packetLength != len) { + System.out.print("..."); + } + System.out.println(""); + } + try { + transport.write(buffer, packetLength); + } catch (IOException e) { + brokenLink(); + throw e; + } + } + + /* + * Connection Identifier: + * * must be first header in packet + * * 0xFFFFFFFF considered invalid - it is up to application + * * can't be sent on connect() request + * * can't be used with Target header in one packet + */ + final void packetAddConnectionID(long id, HeaderSetImpl headers) { + // SPEC: Illegal to send a Connection Id and a Target header + // in the same operation. + if (headers != null + && headers.getHeader(HeaderSetImpl.TARGET) != null) { + return; + } + if (id < 0L || id > 0xFFFFFFFFL) { + return; + } + buffer[packetLength++] = (byte)HEADER_CONNECTION_ID; + encodeInt(id); + } + + /* + * This method is called to handle a situation than header is too large. + * @throws IOException + */ + abstract void headerTooLarge() throws IOException; + + /* + * Adds the specified headers to the packet. + */ + final void packetAddHeaders(HeaderSetImpl headers) + throws IOException { + if (DEBUG) { + System.out.println("packetAddHeaders()"); + } + headerOverflow = false; + newHeader.sendAllQueued(); + + if (headers == null) { + return; + } + + int[] idList = headers.getHeaderList(); + + if (!headers.challenges.isEmpty()) { + newHeader.sendOrQueue(HEADER_AUTH_CHALLENGE, + headers.challenges); + } + + if (idList == null) { + return; + } + + for (int i = 0; i < idList.length; i++) { + int id = idList[i]; + Object value = headers.getHeader(id); + newHeader.sendOrQueue(id, value); + } + } + + void packetAddAuthResponses() throws IOException { + try { + for (int i = 0; i < authResponses.size(); i++) { + if (DEBUG) { + System.out.println( + "packetAddAuthResponses(): added response"); + } + ObexAuth response = (ObexAuth) authResponses.elementAt(i); + int len = response.replyAuthChallenge(buffer, packetLength, + authenticator); + packetLength += len; + } + } catch (ArrayIndexOutOfBoundsException e) { + throw new IOException("auth response request too large"); + } + if (packetLength > maxSendLength) { + throw new IOException("auth response request too large"); + } + } + + final void sendPacket(byte[] head, long connectionId, + HeaderSetImpl headers, boolean allHeaders) throws IOException { + packetBegin(head); + packetAddConnectionID(connectionId, headers); + packetAddAuthResponses(); + packetAddHeaders(headers); + if (allHeaders && !queuedHeaders.isEmpty()) { + queuedHeaders.removeAllElements(); + throw new IOException("packet too large for peer"); + } + packetEnd(); + } + + private final void encodeInt(long val) { + buffer[packetLength++] = (byte)(val >> 24); + buffer[packetLength++] = (byte)(val >> 16); + buffer[packetLength++] = (byte)(val >> 8); + buffer[packetLength++] = (byte)val; + } + + /* + * Reads at least length bytes starting from the given offset + * into the internal buffer. More than length bytes may be + * actually read. The calling function must ensure the buffer is large + * enough to store the entire packet. + * + * @param offset starting offset in the destination buffer + * @param length minimum number of bytes to read + * @return number of bytes actually read + * @throws IOException if an I/O error occurs + */ + private int readLeast(int offset, int length) + throws IOException { + if (transport == null) { + throw new IOException("connection error"); + } + int read = 0; + while (read < length) { + int count = transport.read(cache); + System.arraycopy(cache, 0, buffer, offset + read, count); + read += count; + if (read < length) { + try { + Thread.sleep(TRANSPORT_READ_INTERVAL); + } catch (InterruptedException e) { + throw new InterruptedIOException(e.getMessage()); + } + } + } + return read; + } + + final void recvPacket() throws IOException { + authResponses.removeAllElements(); + if (transport == null) { + throw new IOException("connection error"); + } + try { + int read = readLeast(0, 3); + packetType = buffer[0] & 0xff; + packetLength = ((buffer[1] & 0xff) << 8) + (buffer[2] & 0xff); + if (DEBUG) { + Logging.report(Logging.INFORMATION, 0, + "Expecting " + packetLength + " bytes to arrive..."); + } + if (read < packetLength) { + readLeast(read, packetLength - read); + } + + // dump packet: + if (DEBUG) { + int len = packetLength; + if (!DEBUG2 && len > 20) { + len = 20; + } + + System.out.println("recv: "); + for (int i = 0; i < len; i++) { + System.out.print(" 0x" + + Integer.toHexString(buffer[i] & 0xFF) + .toUpperCase()); + } + if (len != packetLength) System.out.print("..."); + System.out.println(""); + } + } catch (IOException e) { + brokenLink(); + throw e; + } + } + + private final void parseHeader(HeaderSetImpl headers) + throws IOException { + if (DEBUG) { + System.out.println("parseHeader()"); + } + try { + int headerId = buffer[packetOffset++] & 0xff; + int inputType = headerId >> 6; + int outputType = HeaderSetImpl.internalType(headerId); + if (outputType != HeaderSetImpl.TYPE_UNSUPPORTED) { + inputType = outputType; + } + + Object result = null; + + switch (inputType) { + // ids which require special handling + case HeaderSetImpl.TYPE_SPECIAL_TIME_ISO: + try { + result = decodeTime8601(); + } catch (IOException e) { + // IMPL_NOTE: Got invalid time header, + // should probably just ignore it. + } + break; + + case HeaderSetImpl.TYPE_SPECIAL_TIME_4: + long date = decodeInt(); + Calendar cal = Calendar.getInstance(utcTimeZone); + cal.setTime(new Date(date * 1000L)); + result = cal; + packetOffset += 4; + break; + + case HeaderSetImpl.TYPE_SPECIAL_TYPE: + int len = decodeLength16(packetOffset) - 3; + packetOffset += 2; + if (buffer[packetOffset + len - 1] != 0) { + throw new IOException( + "protocol error, " + + "type field not null terminated"); + } + result = new String(buffer, packetOffset, len - 1, + "ISO-8859-1"); + packetOffset += len; + break; + + // normal ids + case HeaderSetImpl.TYPE_LONG: + result = new Long(decodeInt()); + packetOffset += 4; + break; + + case HeaderSetImpl.TYPE_UNICODE: + len = decodeLength16(packetOffset) - 3; + packetOffset += 2; + if (len < 2 || buffer[packetOffset + len - 1] != 0 + || buffer[packetOffset + len - 2] != 0) { + throw new IOException("protocol error, " + + "unicode string is not null terminated"); + } + result = new String(buffer, + packetOffset, len - 2, "UTF-16BE"); + // result = new String(buffer, packetOffset, len, + // "ISO-8859-1"); + packetOffset += len; + break; + + case HeaderSetImpl.TYPE_BYTEARRAY: + len = decodeLength16(packetOffset) - 3; + packetOffset += 2; + result = new byte[len]; + System.arraycopy(buffer, packetOffset, result, 0, len); + packetOffset += len; + break; + + case HeaderSetImpl.TYPE_BYTE: + result = new Byte(buffer[packetOffset++]); + break; + + case HeaderSetImpl.TYPE_AUTH_CHALLENGE: + len = decodeLength16(packetOffset); + ObexAuth response = + ObexAuth.parseAuthChallenge(buffer, packetOffset-1, + len); + if (response != null) + authResponses.addElement(response); + packetOffset += len - 1; + return; + + case HeaderSetImpl.TYPE_AUTH_RESPONSE: + len = decodeLength16(packetOffset); + boolean good = + ObexAuth.checkAuthResponse(buffer, packetOffset-1, len, + this, authChallenges); + if (good) authFailed = false; + packetOffset += len - 1; + if (DEBUG) { + System.out.println("checkAuthResponse() = " + good); + } + return; + } + + if (packetOffset > packetLength) { + throw new IOException("protocol error"); + } + + if (outputType != HeaderSetImpl.TYPE_UNSUPPORTED) { + headers.setHeader(headerId, result); + } else if (DEBUG) { + System.out.println("unsupported header id = 0x" + + Integer.toHexString(headerId).toUpperCase()); + } + } catch (ArrayIndexOutOfBoundsException e) { + throw new IOException("protocol error"); + } + } + + /* + * Called when requested authentication failed. + * Implemented on server to call handler. + */ + abstract void onAuthenticationFailure(byte[] username) throws IOException; + + void onMissingAuthResponse() throws IOException {} + + boolean shouldSendAuthResponse() { + return (packetType == ResponseCodes.OBEX_HTTP_UNAUTHORIZED) + && (authResponses.size() != 0); + } + + /* + * Parser all packet headers, BODY headers should not apear + * and silently ignored. + */ + final void parsePacketHeaders(HeaderSetImpl headers, + int offset) throws IOException { + if (DEBUG) { + System.out.println("parsePacketHeaders()"); + } + + packetOffset = offset; + headers.packetType = buffer[0] & 0xFF; + + parseConnectionID(); + + while (packetOffset != packetLength) { + parseHeader(headers); + } + parseEnd(); + } + + private final void parseConnectionID() { + + int headerId = buffer[packetOffset] & 0xFF; + // parse connection ID + if (packetOffset + 5 > packetLength + || headerId != HEADER_CONNECTION_ID) { + return; + } + packetOffset++; + long id = decodeInt(); + packetOffset += 4; + setConnectionID(id); + } + + public abstract void setConnectionID(long id); + public abstract long getConnectionID(); + + /* + * Begin parsing packet headers in packet possibly containing BODY + * (data fields). + */ + final void parsePacketDataBegin(HeaderSetImpl headers, int offset) { + if (DEBUG) { + System.out.println("parsePacketDataBegin()"); + } + packetOffset = offset; + headers.packetType = buffer[0] & 0xFF; + + parseConnectionID(); + dataOffset = packetOffset; + } + + /* + * Parse packet headers, put BODY field content (data) in specified output + * array. If output is null, search for a BODY block and return 1 if it is + * found. + * @return number of bytes put in output. + */ + final int parsePacketData(HeaderSetImpl headers, + byte[] output, int outputOffset, int outputLength) + throws IOException { + if (DEBUG2) { + System.out.println("parsePacketData()"); + } + int result = 0; + while (true) { + int len = packetOffset - dataOffset; + if (DEBUG2) { + System.out.print("packetOffset = "+packetOffset+ + " dataOffset = " +dataOffset); + System.out.println(" len = " + len); + } + if (len > 0) { + if (output == null) { + // special case for serching first data block + // without actual read + return 1; + } + if (outputLength == 0) { + return result; + } + if (len > outputLength) len = outputLength; + System.arraycopy(buffer, dataOffset, + output, outputOffset, len); + outputOffset += len; + outputLength -= len; + dataOffset += len; + result += len; + continue; + } + + if (DEBUG) { + System.out.println("packetOffset = " + packetOffset + +" packetLength = " + packetLength); + } + if (packetOffset == packetLength) { + return result; + } + int headerId = buffer[packetOffset] & 0xff; + + if (headerId == HEADER_BODY || headerId == HEADER_EOFBODY) { + isEof = (headerId == HEADER_EOFBODY); + dataOffset = packetOffset + 3; + int length = decodeLength16(packetOffset + 1); + if (packetOffset + length > packetLength) { + throw new IOException("protocol error"); + } + packetOffset += length; + continue; + } + + parseHeader(headers); + dataOffset = packetOffset; + } + } + + final void parseEnd() throws IOException { + if (DEBUG) { + System.out.println("parseEnd()"); + } + if (authFailed) { + authFailed = false; + onMissingAuthResponse(); + } + } + + final int decodeLength16(int off) { + return ((((int)buffer[off]) & 0xFF) << 8) + + (((int)buffer[off + 1]) & 0xFF); + } + + private final long decodeInt() { + return ((buffer[packetOffset+0]& 0xffl) << 24) + + ((buffer[packetOffset+1]& 0xffl) << 16) + + ((buffer[packetOffset+2]& 0xffl) << 8) + + (buffer[packetOffset+3]& 0xffl); + } + + private final Calendar decodeTime8601() throws IOException { + int year, month, date, hour, minute, second; + + int len = decodeLength16(packetOffset) - 3; + packetOffset += 2; + + if (len < 15 || len > 16 + || buffer[packetOffset + 8] != 0x54 // 'T' + || (len == 16 && buffer[packetOffset + 15] != 0x5A)) { // 'Z' + packetOffset += len; + throw new IOException("corrupted time header"); + } + for (int i = 0; i < 14; i++) { + if (i == 8) continue; + int chr = buffer[packetOffset + i] - 0x30; // '0' + if (chr < 0 || chr > 9) { + packetOffset += len; + throw new IOException("corrupted time header"); + } + } + + year = (buffer[packetOffset+0] - 0x30) * 1000 + + (buffer[packetOffset+1] - 0x30) * 100 + + (buffer[packetOffset+2] - 0x30) * 10 + + (buffer[packetOffset+3] - 0x30); + month = (buffer[packetOffset+4] - 0x30) * 10 + + (buffer[packetOffset+5] - 0x30); + date = (buffer[packetOffset+6] - 0x30) * 10 + + (buffer[packetOffset+7] - 0x30); + + hour = (buffer[packetOffset+9] - 0x30) * 10 + + (buffer[packetOffset+10] - 0x30); + minute = (buffer[packetOffset+11] - 0x30) * 10 + + (buffer[packetOffset+12] - 0x30); + second = (buffer[packetOffset+13] - 0x30) * 10 + + (buffer[packetOffset+14] - 0x30); + + // is check validness of time fields required? + + Calendar cal; + // 'len' value 15 means local time, + // 16 means UTC (in this case time string has 'Z' suffix) + if (len == 16) { + cal = Calendar.getInstance(utcTimeZone); + } else { + cal = Calendar.getInstance(); + } + cal.set(Calendar.YEAR, year); + cal.set(Calendar.MONTH, month - 1); // Calendar.JANUARY = 0 + cal.set(Calendar.DATE, date); + + // ISO 8601 standard uses the 24-hour clock + // Therefore you use HOUR_OF_DAY not HOUR + cal.set(Calendar.HOUR_OF_DAY, hour); + + cal.set(Calendar.MINUTE, minute); + cal.set(Calendar.SECOND, second); + + // set milliseconds to zero since + // ISO 8601 uses only second precision + cal.set(Calendar.MILLISECOND, 0); + + packetOffset += len; + return cal; + } + + static int validateStatus(int status) { + switch (status) { + case ResponseCodes.OBEX_DATABASE_FULL: + case ResponseCodes.OBEX_DATABASE_LOCKED: + case ResponseCodes.OBEX_HTTP_ACCEPTED: + case ResponseCodes.OBEX_HTTP_BAD_GATEWAY: + case ResponseCodes.OBEX_HTTP_BAD_METHOD: + case ResponseCodes.OBEX_HTTP_BAD_REQUEST: + case ResponseCodes.OBEX_HTTP_CONFLICT: + case ResponseCodes.OBEX_HTTP_CREATED: + case ResponseCodes.OBEX_HTTP_ENTITY_TOO_LARGE: + case ResponseCodes.OBEX_HTTP_FORBIDDEN: + case ResponseCodes.OBEX_HTTP_GATEWAY_TIMEOUT: + case ResponseCodes.OBEX_HTTP_GONE: + case ResponseCodes.OBEX_HTTP_INTERNAL_ERROR: + case ResponseCodes.OBEX_HTTP_LENGTH_REQUIRED: + case ResponseCodes.OBEX_HTTP_MOVED_PERM: + case ResponseCodes.OBEX_HTTP_MOVED_TEMP: + case ResponseCodes.OBEX_HTTP_MULT_CHOICE: + case ResponseCodes.OBEX_HTTP_NO_CONTENT: + case ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE: + case ResponseCodes.OBEX_HTTP_NOT_AUTHORITATIVE: + case ResponseCodes.OBEX_HTTP_NOT_FOUND: + case ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED: + case ResponseCodes.OBEX_HTTP_NOT_MODIFIED: + case ResponseCodes.OBEX_HTTP_OK: + case ResponseCodes.OBEX_HTTP_PARTIAL: + case ResponseCodes.OBEX_HTTP_PAYMENT_REQUIRED: + case ResponseCodes.OBEX_HTTP_PRECON_FAILED: + case ResponseCodes.OBEX_HTTP_PROXY_AUTH: + case ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE: + case ResponseCodes.OBEX_HTTP_RESET: + case ResponseCodes.OBEX_HTTP_SEE_OTHER: + case ResponseCodes.OBEX_HTTP_TIMEOUT: + case ResponseCodes.OBEX_HTTP_UNAUTHORIZED: + case ResponseCodes.OBEX_HTTP_UNAVAILABLE: + case ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE: + case ResponseCodes.OBEX_HTTP_USE_PROXY: + case ResponseCodes.OBEX_HTTP_VERSION: + return status; + default: + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } +} diff --git a/java/midp/com/sun/jsr082/obex/ObexTransport.java b/java/midp/com/sun/jsr082/obex/ObexTransport.java new file mode 100644 index 000000000..a4ec5f821 --- /dev/null +++ b/java/midp/com/sun/jsr082/obex/ObexTransport.java @@ -0,0 +1,85 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.obex; + +import javax.microedition.io.Connection; +import java.io.IOException; + +public interface ObexTransport extends Connection { + + /* + * Reads the packet data into specified buffer. + *

+ * If the specified buffer length is 0, then 0 data + * will be read into this buffer, and the rest of packet + * data is lost. + *

+ * + * + * @param inData the data array to fill with received bytes. + * + * @exception IOException if a local or remote connection + * is closed or I/O error has happen. + * + * @exception NullPointerException if the specified buffer is null. + */ + public int read(byte[] inData) throws IOException; + + /* + * + * + * @param outData the buffer with the data to be sent. + * + * @param len the number of bytes to be sent. + * + * @exception IOException if a local or remote connection + * is closed or I/O error has happen. + * + * @exception NullPointerException if the specified buffer is null. + */ + public void write(byte[] outData, int len) throws IOException; + + /* + * Determines the amount of data (maximum packet size) that can + * be successfully sent in a single write operation. If the size + * of data is greater than the maximum packet size, then then only + * the first maximum packet size bytes of the packet are sent, + * and the rest will be discarded. + *

+ * If the returned values is 0, this means the transport + * implementation is based on a stream protocol, i.e. + * any packet size may be used. + * + * @return the maximum number of bytes that can be sent/received + * in a single call to read()/ write() without losing any data. + */ + public int getMaximumPacketSize(); + + /* + * Get underlying connection. + */ + public Connection getUnderlyingConnection(); +} // end of interface 'ObexTransport' definition diff --git a/java/midp/com/sun/jsr082/obex/ObexTransportNotifier.java b/java/midp/com/sun/jsr082/obex/ObexTransportNotifier.java new file mode 100644 index 000000000..6b6f9b7a5 --- /dev/null +++ b/java/midp/com/sun/jsr082/obex/ObexTransportNotifier.java @@ -0,0 +1,39 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.obex; + +import javax.microedition.io.Connection; +import java.io.IOException; + +public interface ObexTransportNotifier extends Connection { + + /* + * Accepts transport layer connections. + */ + public ObexTransport acceptAndOpen() throws IOException; + + public Connection getUnderlyingConnection(); +} // end of interface 'ObexTransportNotifier' definition diff --git a/java/midp/com/sun/jsr082/obex/QueuedHeader.java b/java/midp/com/sun/jsr082/obex/QueuedHeader.java new file mode 100644 index 000000000..6510af17d --- /dev/null +++ b/java/midp/com/sun/jsr082/obex/QueuedHeader.java @@ -0,0 +1,342 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.obex; + +import java.util.Calendar; +import java.util.Vector; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +public class QueuedHeader { + + /* Debug information, should be false for RR. */ + private static final boolean DEBUG = false; + + private ObexPacketStream stream; + private int type; + private Object value; + byte[] buffer; + int packetLength; + + QueuedHeader(ObexPacketStream stream) { + this.stream = stream; + } + + /* + * + * @param type + * @param value + * @throws IOException + */ + void sendOrQueue(int type, Object value) throws IOException { + if (stream.moreHeaders && send(type, value)) { + stream.challengesToSend = true; + return; + } + queue(type, value); + } + + /* + * Adds the header to queue in ObexPacketStream. + * @param type header's type defined in HeaderSetImpl class. + * @param value header's value. + * @throws IOException thrown if the header is too large. + */ + void queue(int type, Object value) throws IOException { + int maxSendLength = stream.maxSendLength; + boolean fail = false; + + switch (HeaderSetImpl.internalType(type)) { + // ids which require special handling + case HeaderSetImpl.TYPE_SPECIAL_TIME_ISO: + case HeaderSetImpl.TYPE_SPECIAL_TIME_4: + case HeaderSetImpl.TYPE_LONG: + case HeaderSetImpl.TYPE_BYTE: + break; + + case HeaderSetImpl.TYPE_SPECIAL_TYPE: + // type8 + len16 + head + len16 + bytes[length] + zero8 + if (((String)value).length() + 7 > maxSendLength) { + fail = true; + } + break; + + case HeaderSetImpl.TYPE_UNICODE: + // type8 + len16 + head + len16 + bytes[length] + zero16 + if ((((String)value).length() << 1) + 8 > maxSendLength) { + fail = true; + } + break; + + case HeaderSetImpl.TYPE_BYTEARRAY: + // type8 + len16 + head + len16 + bytes[length] + if (((byte[])value).length + 6 > maxSendLength) { + fail = true; + } + break; + + case HeaderSetImpl.TYPE_AUTH_CHALLENGE: + Vector challenges = (Vector)value; + if (challenges.isEmpty()) + return; + int len = 0; + for (int i = 0; i < challenges.size(); i++) { + len += ((ObexAuth) challenges.elementAt(i)) + .prepareChallenge(); + } + if (len + 3 > maxSendLength) { + fail = true; + } + break; + } + + if (fail) { + stream.headerTooLarge(); + return; + } + this.type = type; + this.value = value; + newHeader(); + } + + boolean trySendAgain() throws IOException { + return send(type, value); + } + + boolean send(int type, Object value) throws IOException { + buffer = stream.buffer; + packetLength = stream.packetLength; + int maxSendLength = stream.maxSendLength; + if (DEBUG) { + System.out.println("Sending header 0x" + Integer.toHexString(type)); + } + + try { + buffer[packetLength++] = (byte)type; + switch (HeaderSetImpl.internalType(type)) { + // ids which require special handling + case HeaderSetImpl.TYPE_SPECIAL_TIME_ISO: + encodeTime8601((Calendar)value); + break; + + case HeaderSetImpl.TYPE_SPECIAL_TIME_4: + encodeInt(((Calendar)value).getTime().getTime() / 1000L); + break; + + case HeaderSetImpl.TYPE_SPECIAL_TYPE: + byte[] str = ((String)value).getBytes("ISO-8859-1"); + + // head + len16 + bytes[length] + zero8 + encodeLength16(str.length + 4); + System.arraycopy(str, 0, buffer, packetLength, + str.length); + packetLength += str.length; + // null terminator required by protocol spec + buffer[packetLength++] = 0; + break; + + case HeaderSetImpl.TYPE_AUTH_CHALLENGE: + packetLength--; + Vector challenges = (Vector) value; + + stream.authFailed = false; + + for (int i = 0; i < challenges.size(); i++) { + ObexAuth auth = (ObexAuth) challenges.elementAt(i); + packetLength += auth.addChallenge(buffer, packetLength); + } + + // need to be shure - challenges are added + if (packetLength > maxSendLength) { + break; + } + + // if we still there, then authChallenges + // successfully added + for (int i = 0; i < challenges.size(); i++) { + stream.authFailed = true; + stream.authChallenges.addElement( + challenges.elementAt(i)); + } + break; + + // normal ids + + // 4 byte integer values, range (0xC0 - 0xFF) + // system and user defined + case HeaderSetImpl.TYPE_LONG: + encodeInt(((Long)value).longValue()); + break; + + // unicode encoded string values, range (0x00 - 0x3F) + // system and user defined + case HeaderSetImpl.TYPE_UNICODE: + str = ((String)value).getBytes("UTF-16BE"); + // str = ((String)value).getBytes("ISO-8859-1"); + + // head + len16 + bytes[length] + zero16 + encodeLength16(str.length + 5); + System.arraycopy(str, 0, buffer, packetLength, + str.length); + packetLength += str.length; + // end of unicode string + buffer[packetLength++] = 0; + buffer[packetLength++] = 0; + break; + + // byte[] array values, range (0x40 - 0x7F) + // system and user defined + case HeaderSetImpl.TYPE_BYTEARRAY: + byte[] array = (byte[]) value; + + // head + len16 + bytes[length] + encodeLength16(array.length + 3); + System.arraycopy(array, 0, buffer, packetLength, + array.length); + packetLength += array.length; + stream.containsTargetHeader |= + (type == HeaderSetImpl.TARGET); + break; + + // byte values, range (0x80 - 0xBF) + // user defined + case HeaderSetImpl.TYPE_BYTE: + buffer[packetLength++] = + (byte)((Byte)value).byteValue(); + break; + + // case HeaderSetImpl.TYPE_UNSUPPORTED: + default: + if (DEBUG) { + System.out.println("wrong or unsupported header"); + } + } + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e.toString()); + } catch (ArrayIndexOutOfBoundsException e) { + stream.moreHeaders = false; + return false; + } + if (packetLength > maxSendLength) { + stream.moreHeaders = false; + return false; + } + + // finally closing body headers if opened + if (stream.dataOpened) { + int dataOffset = stream.dataOffset; + int len = stream.packetLength - dataOffset; + buffer[dataOffset+1] = (byte)(len >> 8); + buffer[dataOffset+2] = (byte)len; + stream.dataOpened = false; + stream.dataClosed = true; + } + stream.packetLength = packetLength; + + // gc header value + this.value = null; + if (DEBUG) { + System.out.println(" -> sent!"); + } + return true; + } + + private void newHeader() { + stream.queuedHeaders.addElement(this); + if (stream.emptyHeadersPool.empty()) { + stream.newHeader = new QueuedHeader(stream); + } else { + stream.newHeader = (QueuedHeader) stream.emptyHeadersPool.pop(); + } + } + + boolean sendAllQueued() throws IOException { + while (stream.queuedHeaders.size() > 0 && stream.moreHeaders) { + QueuedHeader header = (QueuedHeader) + stream.queuedHeaders.firstElement(); + boolean res = header.trySendAgain(); + if (!res) { + return false; + } + stream.queuedHeaders.removeElementAt(0); + } + return true; + } + + private final void encodeInt(long val) { + buffer[packetLength++] = (byte)(val >> 24); + buffer[packetLength++] = (byte)(val >> 16); + buffer[packetLength++] = (byte)(val >> 8); + buffer[packetLength++] = (byte)val; + } + + private final void encodeLength16(int value) { + buffer[packetLength++] = (byte)(value >> 8); + buffer[packetLength++] = (byte)value; + } + + private final void encodeTime8601(Calendar cal) { + encodeLength16(19); + + // copy calendar as it can be with wrong timezone. + Calendar cal2 = Calendar.getInstance(ObexPacketStream.utcTimeZone); + cal2.setTime(cal.getTime()); + int year = cal2.get(Calendar.YEAR); + int month = cal2.get(Calendar.MONTH) + 1; // Calendar.JANUARY = 0 + int date = cal2.get(Calendar.DATE); + int hour = cal2.get(Calendar.HOUR_OF_DAY); + int minute = cal2.get(Calendar.MINUTE); + int second = cal2.get(Calendar.SECOND); + int zero = 0x30; // zero ('0') code in latin1 + + if (year < 0 || year > 9999) { + if (DEBUG) { + System.out.println("wrong date header"); + } + } + buffer[packetLength++] = (byte)(year / 1000 + zero); + year = year % 1000; + buffer[packetLength++] = (byte)(year / 100 + zero); + year = year % 100; + buffer[packetLength++] = (byte)(year / 10 + zero); + year = year % 10; + buffer[packetLength++] = (byte)(year + zero); + buffer[packetLength++] = (byte)(month / 10 + zero); + buffer[packetLength++] = (byte)(month % 10 + zero); + buffer[packetLength++] = (byte)(date / 10 + zero); + buffer[packetLength++] = (byte)(date % 10 + zero); + buffer[packetLength++] = 0x54; // 'T' code in latin1 + buffer[packetLength++] = (byte)(hour / 10 + zero); + buffer[packetLength++] = (byte)(hour % 10 + zero); + buffer[packetLength++] = (byte)(minute / 10 + zero); + buffer[packetLength++] = (byte)(minute % 10 + zero); + buffer[packetLength++] = (byte)(second / 10 + zero); + buffer[packetLength++] = (byte)(second % 10 + zero); + buffer[packetLength++] = (byte)0x5A; // 'Z' code in latin1 + } +} + + diff --git a/java/midp/com/sun/jsr082/obex/SSLWrapper.java b/java/midp/com/sun/jsr082/obex/SSLWrapper.java new file mode 100644 index 000000000..3b8cfd2b1 --- /dev/null +++ b/java/midp/com/sun/jsr082/obex/SSLWrapper.java @@ -0,0 +1,68 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.obex; + +// J2ME security classes +import com.sun.j2me.crypto.MessageDigest; +import com.sun.j2me.crypto.NoSuchAlgorithmException; +import com.sun.j2me.crypto.DigestException; + +import java.io.IOException; + +/* + * The platform dependent class which provides a wrapper for + * security API in J2ME or J2SE. + */ +final class SSLWrapper { + private MessageDigest md5; + + SSLWrapper() throws IOException { + try { + md5 = new MessageDigest("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new IOException(e.getMessage()); + } + } + + void update(byte[] input, int offset, int length) { + md5.update(input, offset, length); + } + + void doFinal(byte[] srcData, int srcOff, int srcLen, byte[] dstData, + int dstOff) { + + if (srcLen != 0) { + md5.update(srcData, srcOff, srcLen); + } + + try { + md5.digest(dstData, dstOff, dstData.length - dstOff); + } catch (DigestException e) { + // Buffer too short + throw new RuntimeException("output buffer too short"); + } + } +} // end of class 'SSLWrapper' definition diff --git a/java/midp/com/sun/jsr082/obex/ServerConnectionImpl.java b/java/midp/com/sun/jsr082/obex/ServerConnectionImpl.java new file mode 100644 index 000000000..00d0d2b60 --- /dev/null +++ b/java/midp/com/sun/jsr082/obex/ServerConnectionImpl.java @@ -0,0 +1,397 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.obex; + +import javax.obex.Authenticator; +import javax.obex.ServerRequestHandler; +import javax.obex.ResponseCodes; +import javax.obex.HeaderSet; +import java.io.IOException; +import javax.microedition.io.Connection; + +class ServerConnectionImpl extends ObexPacketStream + implements Connection, Runnable { + + /* Debug information, should be false for RR. */ + private static final boolean DEBUG = false; + + ServerRequestHandler handler; + + private int owner; + private boolean isConnected; + + private long connId; + + /* Current operation header size overflow flag. */ + boolean operationHeadersOverflow = false; + + /* Current operation state. */ + boolean operationClosed; + + void headerTooLarge() throws IOException { + operationHeadersOverflow = true; + operationClosed = true; + } + + ServerConnectionImpl(ObexTransport transport, + ServerRequestHandler handler, Authenticator auth) + throws IOException { + super(transport); + this.handler = handler; + isClient = false; + authenticator = auth; + + // owner field of all created HeaderSets + owner = HeaderSetImpl.OWNER_SERVER; + new Thread(this).start(); + } + + public void run() { + try { + while (processRequest()) { + // if connection closed + if (isClosed()) { + break; + } + } + } catch (Throwable t) { + if (DEBUG) { + System.out.println("ServerConnectionImpl thread exception"); + t.printStackTrace(); + } + } + if (DEBUG) { + System.out.println("ServerConnectionImpl: client disconnected"); + } + close(); + } + + /* + * Modified sendPacket() function. + * If packet is too large - sends OBEX_HTTP_REQ_TOO_LARGE response. + */ + private int sendResponsePacket(byte[] head, + HeaderSetImpl headers) throws IOException { + int status = head[0] & 0xFF; + packetBegin(head); + packetAddConnectionID(getConnectionID(), headers); + packetAddAuthResponses(); + packetAddHeaders(headers); + if (!queuedHeaders.isEmpty() || operationHeadersOverflow) { + queuedHeaders.removeAllElements(); + setPacketType(ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE); + } + packetEnd(); + return status; + } + + private void doConnect() throws IOException { + // NOTE client may not authenticated the server and wants to + // so allow multiple connect requests + HeaderSetImpl inputHeaderSet = new HeaderSetImpl(owner); + HeaderSetImpl responseHeaderSet = new HeaderSetImpl(owner); + + // server side check + if (buffer[3] != 0x10 || packetLength < 7) { + // IMPL_NOTE: It is not decided what to do if the OBEX version number + // is different from the one we support (which is presumably 1.0). + // Windows uses version 1.2, Linux uses version 1.1, and we + // probably want to work with both. + // throw new IOException("unsupported client obex version"); + } + // ignore flags + // save maximum client supported packet size + maxSendLength = decodeLength16(5); + + if (maxSendLength > OBEX_MAXIMUM_PACKET_LENGTH) { + maxSendLength = OBEX_MAXIMUM_PACKET_LENGTH; + } + parsePacketHeaders(inputHeaderSet, 7); + // processMissingAuthentications(); + + int status = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + + try { + status = handler.onConnect(inputHeaderSet, + responseHeaderSet); + } catch (Throwable t) { + t.printStackTrace(); + } + status = validateStatus(status); + + if (status != ResponseCodes.OBEX_HTTP_OK) { + // lets client will authenticate first + authResponses.removeAllElements(); + } + + byte[] head = new byte[] { + (byte)status, + 0, 0, // length will be here + 0x10, // version 1.0 of OBEX + 0x0, // flags + (byte) (OBEX_MAXIMUM_PACKET_LENGTH / 0x100), // maximum client + (byte) (OBEX_MAXIMUM_PACKET_LENGTH % 0x100), // supported packet + // length + }; + + status = sendResponsePacket(head, responseHeaderSet); + + if (status == ResponseCodes.OBEX_HTTP_OK) { + isConnected = true; + } + } + + private boolean notConnected() throws IOException { + if (!isConnected) { + HeaderSetImpl headers = new HeaderSetImpl(owner); + headers.setHeader(HeaderSet.DESCRIPTION, "not connected"); + sendPacket(PACKET_BAD_REQUEST, getConnectionID(), headers, true); + return true; + } + return false; + } + + void onAuthenticationFailure(byte[] username) throws IOException { + try { + if (DEBUG) { + System.out.println("ServerConnectionImpl:" + + " handler.onAuthenticationFailure()"); + } + handler.onAuthenticationFailure(username); + } catch (Throwable t) { + t.printStackTrace(); + } + operationClosed = true; + } + + private void doDisconnect() throws IOException { + if (notConnected()) { + return; + } + HeaderSetImpl inputHeaderSet = new HeaderSetImpl(owner); + HeaderSetImpl responseHeaderSet = new HeaderSetImpl(owner); + + parsePacketHeaders(inputHeaderSet, 3); + // processMissingAuthentications(); + + try { + handler.onDisconnect(inputHeaderSet, responseHeaderSet); + } catch (Throwable t) { + t.printStackTrace(); + } + byte[] head = new byte[] { + (byte) ResponseCodes.OBEX_HTTP_OK, + 0, 0, // length will be here + }; + + int status = sendResponsePacket(head, responseHeaderSet); + + if (status == ResponseCodes.OBEX_HTTP_OK) { + isConnected = false; + } + } + + private void doPut(HeaderSetImpl inputHeaderSet) throws IOException { + int status = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + ServerOperation op = + new ServerOperation(this, inputHeaderSet); + try { + status = handler.onPut(op); + } catch (Throwable t) { + t.printStackTrace(); + } + status = validateStatus(status); + if (operationHeadersOverflow) { + status = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; + } + op.destroy(status); + } + + private void doDelete(HeaderSetImpl inputHeaderSet) throws IOException { + HeaderSetImpl responseHeaderSet = new HeaderSetImpl(owner); + int status = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + try { + status = handler.onDelete(inputHeaderSet, responseHeaderSet); + } catch (Throwable t) { + t.printStackTrace(); + } + status = validateStatus(status); + byte[] head = new byte[] { + (byte)status, + 0, 0, // length will be here + }; + + sendResponsePacket(head, responseHeaderSet); + } + + private void doPutOrDelete() throws IOException { + if (notConnected()) { + return; + } + HeaderSetImpl inputHeaderSet = new HeaderSetImpl(owner); + + int mode = ServerOperation.waitForData(this, + inputHeaderSet, OPCODE_PUT); + + switch (mode) { + case 0: sendPacket(PACKET_SUCCESS, getConnectionID(), null, true); + return; + case 1: doPut(inputHeaderSet); return; + case 2: doDelete(inputHeaderSet); return; + default:return; + } + } + + private void doGet() throws IOException { + if (notConnected()) { + return; + } + int status = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + ServerOperation op = + new ServerOperation(this); + try { + status = handler.onGet(op); + } catch (Throwable t) { + t.printStackTrace(); + } + status = validateStatus(status); + if (operationHeadersOverflow) { + status = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; + } + op.destroy(status); + } + + private void doSetPath() throws IOException { + if (notConnected()) { + return; + } + HeaderSetImpl inputHeaderSet = new HeaderSetImpl(owner); + HeaderSetImpl responseHeaderSet = new HeaderSetImpl(owner); + + // check flags + boolean create = ((buffer[3] & 2) == 0); + boolean backup = ((buffer[3] & 1) == 1); + + parsePacketHeaders(inputHeaderSet, 5); + // processMissingAuthentications(); + int status = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + try { + status = handler.onSetPath(inputHeaderSet, + responseHeaderSet, backup, create); + } catch (Throwable t) { + t.printStackTrace(); + } + status = validateStatus(status); + byte[] head = new byte[] { + (byte)status, + 0, 0, // length will be here + }; + sendResponsePacket(head, responseHeaderSet); + } + + /* + * Process one client request + * @return false when connection closed + */ + private boolean processRequest() throws IOException { + try { + recvPacket(); + } catch (IOException e) { + return false; + } + HeaderSetImpl inputHeaderSet = new HeaderSetImpl(owner); + operationHeadersOverflow = false; + operationClosed = false; + isEof = false; + + switch (packetType) { + case OPCODE_CONNECT: + doConnect(); + break; + + case OPCODE_DISCONNECT: + doDisconnect(); + break; + + case OPCODE_PUT: + case OPCODE_PUT | OPCODE_FINAL: + doPutOrDelete(); + break; + + case OPCODE_GET: + case OPCODE_GET | OPCODE_FINAL: + doGet(); + break; + case OPCODE_SETPATH: + doSetPath(); + break; + + case OPCODE_ABORT: + // ignore abort, it is too late, any of the operations is + // finished + byte[] head = new byte[] { + (byte) ResponseCodes.OBEX_HTTP_OK, + 0, 0, // length will be here + }; + sendResponsePacket(head, null); + break; + + default: + // wrong packet received, ignoring + if (DEBUG) { + System.out.println("Wrong packet: id = " + + inputHeaderSet.packetType + " length = " + + packetLength); + } + sendPacket(PACKET_NOT_IMPLEMENTED, getConnectionID(), + null, true); + } + return true; + } + + public void setConnectionID(long id) { + try { // may by overloaded by user and throw exception + connId = id; + handler.setConnectionID(id); + } catch (Throwable e) { + // nothing + } + } + + public long getConnectionID() { + try { // may by overloaded by user and throw exception + long id = handler.getConnectionID(); + if (connId == id) { + return -1; + } + connId = id; + return id; + } catch (Throwable e) { + return -1; + } + } +} + diff --git a/java/midp/com/sun/jsr082/obex/ServerOperation.java b/java/midp/com/sun/jsr082/obex/ServerOperation.java new file mode 100644 index 000000000..720f9ea0a --- /dev/null +++ b/java/midp/com/sun/jsr082/obex/ServerOperation.java @@ -0,0 +1,530 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.obex; + +import javax.obex.Operation; +import javax.obex.HeaderSet; +import javax.obex.ResponseCodes; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +/* + * The class implements server side of put/get operation. + */ +final class ServerOperation implements Operation { + // Debug information, should be false for RR + private static final boolean DEBUG = false; + + private Object lock = new Object(); + private HeaderSetImpl recvHeaders; + private ServerConnectionImpl stream; + private int opcode; + private boolean isGet; + + private boolean isAborted; + private boolean requestEnd; + + private boolean inputStreamOpened = false; + private boolean outputStreamOpened = false; + private boolean inputStreamEof; + private boolean responseIsSent = false; + + private OperationInputStream is = new OperationInputStream(); + private OperationOutputStream os = new OperationOutputStream(); + + private byte[] head = ObexPacketStream.PACKET_CONTINUE; + + + /* Constructor for get operation. */ + ServerOperation(ServerConnectionImpl stream) throws IOException { + this.stream = stream; + opcode = ObexPacketStream.OPCODE_GET; + isGet = true; + recvHeaders = new HeaderSetImpl(HeaderSetImpl.OWNER_SERVER); + int mode = waitForData(stream, + recvHeaders, ObexPacketStream.OPCODE_GET); + switch (mode) { + case 0: + isAborted = true; + stream.operationClosed = true; + break; + case 2: + requestEnd = true; + // no data was received + inputStreamEof = true; + stream.packetBegin(head); + // Response packets can contains both TARGET and CONN ID headers + stream.packetAddConnectionID(stream.getConnectionID(), null); + stream.packetAddAuthResponses(); + stream.packetAddHeaders(null); + break; + } + } + + /* Constructor for put operation. */ + ServerOperation(ServerConnectionImpl stream, HeaderSetImpl recvHeaders) { + // data parsing mode already on. + this.stream = stream; + opcode = ObexPacketStream.OPCODE_PUT; + isGet = false; + this.recvHeaders = recvHeaders; + + } + + public DataOutputStream openDataOutputStream() throws IOException { + return new DataOutputStream(openOutputStream()); + } + + public OutputStream openOutputStream() throws IOException { + if (DEBUG) { + System.out.println("server: openOutputStream()"); + } + synchronized (lock) { + if (stream.operationClosed) { + throw new IOException("operation closed"); + } + if (outputStreamOpened) { + throw new IOException("no more output streams available"); + } + if (!requestEnd) { + throw new IOException("input data not read out"); + } + outputStreamOpened = true; + return os; + } + } + + public DataInputStream openDataInputStream() throws IOException { + return new DataInputStream(openInputStream()); + } + + public InputStream openInputStream() throws IOException { + if (DEBUG) { + System.out.println("server: openInputStream()"); + } + synchronized (lock) { + if (stream.operationClosed) { + throw new IOException("operation closed"); + } + if (inputStreamOpened) { + throw new IOException("no more input streams available"); + } + inputStreamOpened = true; + return is; + } + } + + public void abort() throws IOException { + // forbidden on server + throw new IOException("not permitted"); + } + + public HeaderSet getReceivedHeaders() throws IOException { + synchronized (lock) { + if (stream.operationClosed) { + throw new IOException("operation closed"); + } + return new HeaderSetImpl(recvHeaders); + } + } + + public int getResponseCode() throws IOException { + // forbidden on server + throw new IOException("not permitted"); + } + + public String getEncoding() { + return null; // acording to docs + } + + public long getLength() { + Long res = (Long)recvHeaders.getHeader(HeaderSetImpl.LENGTH); + if (res == null) { + return -1; + } + return res.longValue(); + } + + public String getType() { + return (String)recvHeaders.getHeader(HeaderSetImpl.TYPE); + } + + public void close() { + stream.operationClosed = true; + } + + /* + * Called by ServerRequestHandler to finish any activity remaining + * and to return errorcode to client. + */ + void destroy(int status) { + if (DEBUG) { + System.out.println("server: destroy()"); + } + try { + outputStreamOpened = false; + if (!responseIsSent) { + if (!isGet) { + stream.packetBegin(head); + stream.packetAddConnectionID(stream.getConnectionID(), null); + stream.packetAddAuthResponses(); + } + stream.packetAddHeaders(null); + close(); + if (isAborted) status = ResponseCodes.OBEX_HTTP_OK; + stream.setPacketType(status); + stream.packetEnd(); // send final packet + } + } catch (Throwable t) { + // ignore + } + // remaining headers will be lost + stream.queuedHeaders.removeAllElements(); + } + + public void sendHeaders(HeaderSet headers) throws IOException { + if (DEBUG) { + System.out.println("server: sendHeaders()"); + } + synchronized (lock) { + if (stream.operationClosed) { + throw new IOException("operation closed"); + } + if (headers == null) { + throw new NullPointerException("null headerset"); + } + if (!(headers instanceof HeaderSetImpl)) { + throw new IllegalArgumentException("wrong headerset class"); + } + HeaderSetImpl headersImpl = (HeaderSetImpl) headers; + if (!headersImpl.isSendable()) { + throw new IllegalArgumentException( + "not created with createHeaderSet"); + } + + stream.packetAddHeaders(headersImpl); + + if (requestEnd && isGet) { + while (!stream.queuedHeaders.isEmpty()) { + packetExchange(); + } + } + } + } + + private void packetExchange() throws IOException { + if (DEBUG) { + System.out.println("server: packetExchange()"); + } + if (stream.operationHeadersOverflow) { + throw new IOException("operation terminated, too long headers"); + } + if (!requestEnd) { + // reading out input stream + requestEnd = + stream.packetType == (opcode | ObexPacketStream.OPCODE_FINAL); + + // inordenary case: EOF-DATA but no final bit + if (stream.isEof && !requestEnd) { + while (recvHeaders.packetType + == ObexPacketStream.OPCODE_PUT) { + // not final - waiting for final, data not allowed + // after EOFB + stream.sendPacket(head, stream.getConnectionID(), + null, false); + stream.recvPacket(); + stream.parsePacketHeaders(recvHeaders, 3); + } + + if (recvHeaders.packetType + == ObexPacketStream.OPCODE_ABORT) { + stream.operationClosed = true; + isAborted = true; + throw new IOException("operation aborted"); + } + + if (stream.packetType != + (opcode | ObexPacketStream.OPCODE_FINAL)) { + stream.operationClosed = true; + stream.brokenLink(); + throw new IOException("protocol error"); + } + } + if (requestEnd) { + // switch to requestEnd packetExchange mode + stream.packetBegin(head); + stream.packetAddConnectionID(stream.getConnectionID(), null); + stream.packetAddAuthResponses(); + stream.packetAddHeaders(null); + return; + } + // stream.parseEnd(); + stream.sendPacket(ObexPacketStream.PACKET_CONTINUE, + stream.getConnectionID(), null, false); + stream.recvPacket(); + + if (stream.packetType == ObexPacketStream.OPCODE_ABORT) { + stream.parsePacketHeaders(recvHeaders, 3); + isAborted = true; + stream.operationClosed = true; + throw new IOException("operation aborted"); + } + + if ((stream.packetType & ~ObexPacketStream.OPCODE_FINAL) + != opcode) { + stream.operationClosed = true; + stream.brokenLink(); + throw new IOException("protocol error"); + } + stream.parsePacketDataBegin(recvHeaders, 3); + return; + } + stream.packetEnd(); + stream.recvPacket(); + stream.parsePacketHeaders(recvHeaders, 3); + + if (stream.packetType == ObexPacketStream.OPCODE_ABORT) { + // prepare response packet + stream.packetBegin(ObexPacketStream.PACKET_SUCCESS); + stream.packetAddConnectionID(stream.getConnectionID(), null); + stream.packetAddAuthResponses(); + stream.packetAddHeaders(null); + + isAborted = true; + stream.operationClosed = true; + throw new IOException("operation aborted"); + } + + if (stream.packetType == ObexPacketStream.OPCODE_DISCONNECT) { + stream.sendPacket(ObexPacketStream.PACKET_SUCCESS, + stream.getConnectionID(), null, false); + + stream.close(); + return; + } + + if (stream.packetType != (opcode | ObexPacketStream.OPCODE_FINAL)) { + stream.operationClosed = true; + stream.brokenLink(); + throw new IOException("protocol error"); + } + + stream.packetBegin(head); + stream.packetAddConnectionID(stream.getConnectionID(), null); + stream.packetAddAuthResponses(); + stream.packetAddHeaders(null); + } + + static int waitForData(ServerConnectionImpl stream, + HeaderSetImpl inputHeaderSet, int op) throws IOException { + if (DEBUG) { + System.out.println("server: waitForData()"); + } + + // check of errorcode should be done before after data parsing + stream.parsePacketDataBegin(inputHeaderSet, 3); + + // special request to check data availability + int hasData = stream.parsePacketData(inputHeaderSet, null, 0, 0); + + // waiting for data or final bit or abort + while (true) { + if (stream.packetType == ObexPacketStream.OPCODE_ABORT) { + return 0; + } + + if (hasData == 1 || stream.isEof) { + return 1; // has data + } + + if (stream.packetType == (op | ObexPacketStream.OPCODE_FINAL)) { + return 2; // final + } + + if (stream.packetType != op) { + stream.brokenLink(); + throw new IOException("protocol error"); + } + + stream.sendPacket(ObexPacketStream.PACKET_CONTINUE, + stream.getConnectionID(), null, false); + stream.recvPacket(); + + // check of errorcode should be done before after data parsing + stream.parsePacketDataBegin(inputHeaderSet, 3); + + // special request to check data availability + hasData = stream.parsePacketData(inputHeaderSet, null, 0, 0); + } + } + + private class OperationInputStream extends InputStream { + OperationInputStream() {} + + public int read() throws IOException { + byte[] b = new byte[1]; + int len = read(b, 0, 1); + if (len == -1) { + return -1; + } + return b[0] & 0xFF; + } + + public int read(byte[] b, int offset, int len) throws IOException { + if (DEBUG) { + // System.out.println("server: is.read()"); + } + synchronized (lock) { + if (!inputStreamOpened) { + throw new IOException("operation finished"); + } + // Nullpointer check is here + if (len < 0 || offset < 0 || offset + len > b.length) { + throw new ArrayIndexOutOfBoundsException(); + } + + if (len == 0) { + return 0; + } + + if (inputStreamEof) { + return -1; + } + + int result = 0; + while (true) { + int rd = stream.parsePacketData(recvHeaders, b, + offset, len); + if (rd != 0) { + offset += rd; + len -= rd; + result += rd; + if (len == 0) { + if (stream.dataOffset != stream.packetOffset) { + return result; + } + } + } else { + if ((len == 0) && !stream.isEof) { + return result; + } + } + + // need more data, packet is finished + if (stream.isEof) { + inputStreamEof = true; + if (stream.dataOffset == stream.packetOffset) { + requestEnd = stream.packetType == + (opcode | ObexPacketStream.OPCODE_FINAL); + return (result == 0) ? -1 : result; + } else { + return result; + } + } + packetExchange(); + } + } + } + + public void close() throws IOException { + if (DEBUG) { + System.out.println("server: is.close()"); + } + // errorcode unknown yet, + // ServerRequestHandler will send errorcode packet + synchronized (lock) { + inputStreamOpened = false; + inputStreamEof = false; + } + } + } + + private class OperationOutputStream extends OutputStream { + OperationOutputStream() {} + + public void write(int b) throws IOException { + write(new byte[] { (byte)b }, 0, 1); + } + + public void write(byte[] b, int offset, int len) throws IOException { + if (DEBUG) { + // System.out.println("server: os.write()"); + } + synchronized (lock) { + if (!outputStreamOpened) { + throw new IOException("operation finished"); + } + if (len < 0 || offset < 0 || offset + len > b.length) { + throw new ArrayIndexOutOfBoundsException(); + } + while (len > 0) { + int wr = stream.packetAddData(b, offset, len); + if (wr != len) { + packetExchange(); + } + len -= wr; + offset += wr; + } + } + } + + public void flush() throws IOException { + if (DEBUG) { + System.out.println("server: os.flush()"); + } + synchronized (lock) { + if (!outputStreamOpened) { + throw new IOException("operation finished"); + } + if (isGet) { + packetExchange(); + } + } + } + + + public void close() throws IOException { + if (DEBUG) { + System.out.println("server: os.close()"); + } + + synchronized (lock) { + if (outputStreamOpened) { + outputStreamOpened = false; + boolean res = stream.packetEOFBody(); + if (!res) { // error adding EOFB previous packet too long + packetExchange(); + stream.packetEOFBody(); + } + } + // Unknown errorcode yet, not sending: stream.packetEnd(); + } + } + } +} diff --git a/java/midp/com/sun/jsr082/obex/SessionNotifierImpl.java b/java/midp/com/sun/jsr082/obex/SessionNotifierImpl.java new file mode 100644 index 000000000..b1471e988 --- /dev/null +++ b/java/midp/com/sun/jsr082/obex/SessionNotifierImpl.java @@ -0,0 +1,68 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.obex; + +import javax.obex.Authenticator; +import javax.obex.SessionNotifier; +import javax.obex.ServerRequestHandler; +import javax.microedition.io.Connection; +import java.io.IOException; + +public class SessionNotifierImpl implements SessionNotifier { + + private ObexTransportNotifier notifier; + + public SessionNotifierImpl(ObexTransportNotifier notifier) + throws IOException { + this.notifier = notifier; + } + + public Connection acceptAndOpen(ServerRequestHandler handler) + throws IOException { + return acceptAndOpen(handler, null); + } + + public Connection acceptAndOpen(ServerRequestHandler handler, + Authenticator auth) throws IOException { + if (notifier == null) { + throw new IOException("session closed"); + } + if (handler == null) { + throw new NullPointerException("null handler"); + } + ObexTransport transport = notifier.acceptAndOpen(); + return new ServerConnectionImpl(transport, handler, auth); + } + + public void close() throws IOException { + if (notifier != null) notifier.close(); + notifier = null; + } + + public Connection getTransport() { + return notifier.getUnderlyingConnection(); + } +} diff --git a/java/midp/com/sun/jsr082/obex/btgoep/BTGOEPConnection.java b/java/midp/com/sun/jsr082/obex/btgoep/BTGOEPConnection.java new file mode 100644 index 000000000..a871ea9ff --- /dev/null +++ b/java/midp/com/sun/jsr082/obex/btgoep/BTGOEPConnection.java @@ -0,0 +1,175 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.obex.btgoep; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; +import javax.microedition.io.StreamConnection; +import javax.microedition.io.Connection; +import com.sun.j2me.main.Configuration; +import com.sun.jsr082.obex.ObexTransport; + +/* + * Provides underlying stream connection used as transport by shared obex + * implementation. + */ +public class BTGOEPConnection implements ObexTransport { + + private StreamConnection sock; + private InputStream is; + private OutputStream os; + + /* + * Create BTGOEPConnection + * @param sock Stream connection for the transport layer + */ + protected BTGOEPConnection(StreamConnection sock) throws IOException { + this.sock = sock; + is = sock.openInputStream(); + os = sock.openOutputStream(); + } + + /* + * Closes connection as well as the input stream and + * the output stream openning for this connection. + * @throws IOException if I/O error. + */ + public void close() throws IOException { + IOException ioe = null; + + try { + is.close(); + } catch (IOException e) { + ioe = e; + } + + try { + os.close(); + } catch (IOException e) { + ioe = e; + } + + try { + sock.close(); + } catch (IOException e) { + ioe = e; + } + + // catch IOException if any of the above call has thrown one + if (ioe != null) { + throw ioe; + } + } + + /* + * Reads the packet data into specified buffer. + *

+ * If the specified buffer length is 0, then 0 data + * will be read into this buffer, and the rest of packet + * data is lost. + *

+ * + * @param inData the data array to fill with received bytes. + * @exception IOException if a local or remote connection + * is closed or I/O error has happen. + * + * @exception NullPointerException if the specified buffer is null. + */ + public int read(byte[] inData) throws IOException { + readFully(inData, 0, 3); // read header + int packetLength = decodeLength16(inData, 1); + if (packetLength < 3 || packetLength > inData.length) { + throw new IOException("protocol error"); + } + + readFully(inData, 3, packetLength - 3); + return packetLength; + } + + /* + * + * @param outData the buffer with the data to be sent. + * @param len the number of bytes to be sent. + * @exception IOException if a local or remote connection + * is closed or I/O error has happen. + * + * @exception NullPointerException if the specified buffer is null. + */ + public void write(byte[] outData, int len) throws IOException { + os.write(outData, 0, len); + os.flush(); + } + + /* + * Determines the amount of data (maximum packet size) that can + * be successfully sent in a single write operation. If the size + * of data is greater than the maximum packet size, then then only + * the first maximum packet size bytes of the packet are sent, + * and the rest will be discarded. + *

+ * + * @return the maximum number of bytes that can be sent/received + * in a single call to read()/ write() without losing any data. + */ + public int getMaximumPacketSize() { + return Configuration.getIntProperty( + "obex.packetLength.max", 4096); + } + + /* + * Reads up to len bytes of data from the input stream into + * an array of bytes. + * @param array the buffer into which the data is read. + * @param offset the start offset in array b + * at which the data is written. + * @param size the maximum number of bytes to read. + * @exception IOException if an I/O error occurs. + */ + private final void readFully(byte[] array, int offset, int size) + throws IOException { + while (size != 0) { + int count = is.read(array, offset, size); + if (count == -1) { + throw new IOException("read error"); + } + offset += count; + size -= count; + } + } + + private final int decodeLength16(byte[] buffer, int off) { + return ((((int)buffer[off]) & 0xFF) << 8) + + (((int)buffer[off + 1]) & 0xFF); + } + + /* + * Get underlying connection. + */ + public Connection getUnderlyingConnection() { + return sock; + } +} diff --git a/java/midp/com/sun/jsr082/obex/btgoep/BTGOEPNotifier.java b/java/midp/com/sun/jsr082/obex/btgoep/BTGOEPNotifier.java new file mode 100644 index 000000000..a2792cc16 --- /dev/null +++ b/java/midp/com/sun/jsr082/obex/btgoep/BTGOEPNotifier.java @@ -0,0 +1,96 @@ +/* + * + * + * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ +package com.sun.jsr082.obex.btgoep; + +import java.io.IOException; +import javax.bluetooth.*; +import javax.microedition.io.StreamConnectionNotifier; +import javax.microedition.io.StreamConnection; +import javax.microedition.io.Connection; +import com.sun.jsr082.bluetooth.ServiceRecordImpl; +import com.sun.jsr082.obex.ObexTransportNotifier; +import com.sun.jsr082.obex.ObexTransport; + +/* + * Provides underlying stream notifier to shared obex implementation. + */ +public class BTGOEPNotifier implements ObexTransportNotifier { + + /* Keeps notifier for transport layer */ + private StreamConnectionNotifier notifier; + + /* Keeps OBEX UUID for service record construction. */ + static public final DataElement DE_OBEX_UUID = + new DataElement(DataElement.UUID, new UUID(0x0008)); + + /* + * Create BTGOEP Notifier + * @param notifier notifier for transport layer + * @exception IOException if an error occured while service record + * creation + */ + protected BTGOEPNotifier(StreamConnectionNotifier notifier) + throws IOException { + this.notifier = notifier; + } + + /* + * Accepts client connection to the service this notifier is assigned to. + * + * @return connection to a client just accepted on transport layer. + * @exception IOException if an error occured on transport layer. + */ + public ObexTransport acceptAndOpen() throws IOException { + return createTransportConnection( + (StreamConnection)(notifier.acceptAndOpen())); + } + + /* + * Closes this notifier on the transport layer + * @exception IOException if an error occured on transport layer + */ + public void close() throws IOException { + notifier.close(); + } + + /* + * Create btgoep transport connection. + * @param sock transport connection + * @return BTGOEP Connection + */ + protected BTGOEPConnection createTransportConnection( + StreamConnection sock) throws IOException { + return new BTGOEPConnection(sock); + } + + /* + * Get transport connection noifier + * @return transport notifier + */ + public Connection getUnderlyingConnection() { + return notifier; + } +} diff --git a/java/midp/javax/bluetooth/BluetoothConnectionException.java b/java/midp/javax/bluetooth/BluetoothConnectionException.java new file mode 100644 index 000000000..a67932acc --- /dev/null +++ b/java/midp/javax/bluetooth/BluetoothConnectionException.java @@ -0,0 +1,90 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.bluetooth; +import java.io.IOException; + +/* + * This class is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public class BluetoothConnectionException extends IOException { + + // JAVADOC COMMENT ELIDED + public static final int UNKNOWN_PSM = 0x0001; + + // JAVADOC COMMENT ELIDED + public static final int SECURITY_BLOCK = 0x0002; + + // JAVADOC COMMENT ELIDED + public static final int NO_RESOURCES = 0x0003; + + // JAVADOC COMMENT ELIDED + public static final int FAILED_NOINFO = 0x0004; + + // JAVADOC COMMENT ELIDED + public static final int TIMEOUT = 0x0005; + + // JAVADOC COMMENT ELIDED + public static final int UNACCEPTABLE_PARAMS = 0x0006; + + /* Contains the error code specified in constructor. */ + private int status; + + // JAVADOC COMMENT ELIDED + public BluetoothConnectionException(int error) { + this(error, null); + } + + // JAVADOC COMMENT ELIDED + public BluetoothConnectionException(int error, String msg) { + super(msg); + + switch (error) { + case UNKNOWN_PSM: /* falls through */ + case SECURITY_BLOCK: /* falls through */ + case NO_RESOURCES: /* falls through */ + case FAILED_NOINFO: /* falls through */ + case TIMEOUT: /* falls through */ + case UNACCEPTABLE_PARAMS: + status = error; + break; + default: + throw new IllegalArgumentException("Invalid error code: " + error); + } + } + + // JAVADOC COMMENT ELIDED + public int getStatus() { + return status; + } +} // end of class 'BluetoothConnectionException' definition diff --git a/java/midp/javax/bluetooth/BluetoothStateException.java b/java/midp/javax/bluetooth/BluetoothStateException.java new file mode 100644 index 000000000..251801ca2 --- /dev/null +++ b/java/midp/javax/bluetooth/BluetoothStateException.java @@ -0,0 +1,51 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.bluetooth; +import java.io.IOException; + +/* + * This class is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public class BluetoothStateException extends IOException { + + // JAVADOC COMMENT ELIDED + public BluetoothStateException() { + super(); + } + + // JAVADOC COMMENT ELIDED + public BluetoothStateException(String msg) { + super(msg); + } +} // end of class 'BluetoothStateException' definition diff --git a/java/midp/javax/bluetooth/DataElement.java b/java/midp/javax/bluetooth/DataElement.java new file mode 100644 index 000000000..a08bea8f8 --- /dev/null +++ b/java/midp/javax/bluetooth/DataElement.java @@ -0,0 +1,356 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.bluetooth; +import java.util.Vector; + +/* + * This class is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public class DataElement { + + // JAVADOC COMMENT ELIDED + public static final int NULL = 0x0000; + + // JAVADOC COMMENT ELIDED + public static final int U_INT_1 = 0x0008; + + // JAVADOC COMMENT ELIDED + public static final int U_INT_2 = 0x0009; + + // JAVADOC COMMENT ELIDED + public static final int U_INT_4 = 0x000A; + + // JAVADOC COMMENT ELIDED + public static final int U_INT_8 = 0x000B; + + // JAVADOC COMMENT ELIDED + public static final int U_INT_16 = 0x000C; + + // JAVADOC COMMENT ELIDED + public static final int INT_1 = 0x0010; + + // JAVADOC COMMENT ELIDED + public static final int INT_2 = 0x0011; + + // JAVADOC COMMENT ELIDED + public static final int INT_4 = 0x0012; + + // JAVADOC COMMENT ELIDED + public static final int INT_8 = 0x0013; + + // JAVADOC COMMENT ELIDED + public static final int INT_16 = 0x0014; + + // JAVADOC COMMENT ELIDED + public static final int URL = 0x0040; + + // JAVADOC COMMENT ELIDED + public static final int UUID = 0x0018; + + // JAVADOC COMMENT ELIDED + public static final int BOOL = 0x0028; + + // JAVADOC COMMENT ELIDED + public static final int STRING = 0x0020; + + // JAVADOC COMMENT ELIDED + public static final int DATSEQ = 0x0030; + + // JAVADOC COMMENT ELIDED + public static final int DATALT = 0x0038; + + /* Keeps the specified or derived type of the element. */ + private int valueType; + + /* Keeps the boolean value for the type BOOL. */ + private boolean booleanValue; + + /* Keeps the long value for the types *INT*. */ + private long longValue; + + /* + * Keeps the misc type value for the rest of types. + * + * This field also keeps the value for the type DATALT and DATSEQ. + * In this case it's a Vector. The access to the Vector elements + * is synchronized in cldc (according the source code). But, + * this is not documented, so we make a synchronize access + * to this field to fit any cldc implementation. + */ + private Object miscValue; + + // JAVADOC COMMENT ELIDED + public DataElement(int valueType) { + switch (valueType) { + case NULL: /* miscValue = null in this case. */ + break; + case DATALT: /* falls through */ + case DATSEQ: + this.miscValue = new Vector(); + break; + default: + throw new IllegalArgumentException( + "Invalid valueType for this constructor: " + valueType); + } + this.valueType = valueType; + } + + // JAVADOC COMMENT ELIDED + public DataElement(boolean bool) { + valueType = BOOL; + booleanValue = bool; + } + + // JAVADOC COMMENT ELIDED + public DataElement(int valueType, long value) { + long min = 0; + long max = 0; + + switch (valueType) { + case U_INT_1: + max = 0xffL; + break; + case U_INT_2: + max = 0xffffL; + break; + case U_INT_4: + max = 0xffffffffL; + break; + case INT_1: + min = Byte.MIN_VALUE; + max = Byte.MAX_VALUE; + break; + case INT_2: + min = -0x8000L; + max = 0x7fffL; + break; + case INT_4: + min = Integer.MIN_VALUE; + max = Integer.MAX_VALUE; + break; + case INT_8: + min = Long.MIN_VALUE; + max = Long.MAX_VALUE; + break; + default: + throw new IllegalArgumentException( + "Invalid 'valueType' for this constructor: " + valueType); + } + + // check if value in the valid rangle for this type + if (value < min || value > max) { + throw new IllegalArgumentException( + "Invalid 'value' for specified type: " + value); + } + this.valueType = valueType; + this.longValue = value; + } + + // JAVADOC COMMENT ELIDED + public DataElement(int valueType, Object value) { + boolean isCorrectValue = true; + + switch (valueType) { + case URL: /* falls through */ + case STRING: + isCorrectValue = value instanceof String; + break; + case UUID: + isCorrectValue = value instanceof UUID; + break; + case INT_16: /* falls through */ + case U_INT_16: + isCorrectValue = value instanceof byte[] + && ((byte[]) value).length == 16; + break; + case U_INT_8: + isCorrectValue = value instanceof byte[] + && ((byte[]) value).length == 8; + break; + default: + throw new IllegalArgumentException( + "Invalid 'valueType' for this constructor: " + valueType); + } + + // check if value in the valid rangle for this type + if (!isCorrectValue) { + throw new IllegalArgumentException( + "Invalid 'value' for specified type: " + value); + } + this.valueType = valueType; + this.miscValue = value; + } + + // JAVADOC COMMENT ELIDED + public synchronized void addElement(DataElement elem) { + + /* + * We can't optimize this by invoking the + * this.insertElementAt(elem, getSize()), because + * the ClassCastException may be thrown from getSize() + * which gives us improper stack trace. + */ + if (valueType != DATSEQ && valueType != DATALT) { + throw new ClassCastException( + "Invalid element type for this method: " + valueType); + } + + if (elem == null) { + throw new NullPointerException("Specified element is null"); + } + ((Vector) miscValue).addElement(elem); + } + + // JAVADOC COMMENT ELIDED + public synchronized void insertElementAt(DataElement elem, int index) { + if (valueType != DATSEQ && valueType != DATALT) { + throw new ClassCastException( + "Invalid element type for this method: " + valueType); + } + + if (elem == null) { + throw new NullPointerException("Specified element is null"); + } + + /* + * We can't use the Vector.insertElementAt check for out of + * bounds, because Vector throws ArrayIndexOutOfBoundsException + * in this case. + */ + if (index < 0 || index > ((Vector) miscValue).size()) { + throw new IndexOutOfBoundsException( + "Specified index is out of range"); + } + ((Vector) miscValue).insertElementAt(elem, index); + } + + // JAVADOC COMMENT ELIDED + public synchronized int getSize() { + if (valueType != DATSEQ && valueType != DATALT) { + throw new ClassCastException( + "Invalid element type for this method: " + valueType); + } + return ((Vector) miscValue).size(); + } + + // JAVADOC COMMENT ELIDED + public boolean removeElement(DataElement elem) { + if (valueType != DATSEQ && valueType != DATALT) { + throw new ClassCastException( + "Invalid element type for this method: " + valueType); + } + + if (elem == null) { + throw new NullPointerException("Specified element is null"); + } + + /* + * The Bluetooth spec says the two DataElement equals if their + * references are equal. According to cldc1.1 ref impl sources, + * the Vector uses 'equals' call, and the Object.equls uses + * a references compare, so we may not care about doing this here. + */ + return ((Vector) miscValue).removeElement(elem); + } + + // JAVADOC COMMENT ELIDED + public int getDataType() { + return valueType; + } + + // JAVADOC COMMENT ELIDED + public long getLong() { + switch (valueType) { + case U_INT_1: /* falls through */ + case U_INT_2: /* falls through */ + case U_INT_4: /* falls through */ + case INT_1: /* falls through */ + case INT_2: /* falls through */ + case INT_4: /* falls through */ + case INT_8: + break; + default: + throw new ClassCastException( + "Invalid element type for this method: " + valueType); + } + return longValue; + } + + // JAVADOC COMMENT ELIDED + public boolean getBoolean() { + if (valueType != BOOL) { + throw new ClassCastException( + "Invalid element type for this method: " + valueType); + } + return booleanValue; + } + + // JAVADOC COMMENT ELIDED + public synchronized Object getValue() { + Object retValue = miscValue; + + /* + * According to cldc & bluetooth specifications, the String and UUID + * are immutable, so we may not return a clone object to safe + * the stored one. + * + * The Vector.elements() returns an Enumeration, which does not allow + * to break the Vector either. + * + * The array may be modified by reference, so we have to return + * a clone. + */ + switch (valueType) { + case URL: /* falls through */ + case STRING: /* falls through */ + case UUID: + break; + case DATALT: /* falls through */ + case DATSEQ: + retValue = ((Vector) miscValue).elements(); + break; + case U_INT_8: /* falls through */ + case U_INT_16: /* falls through */ + case INT_16: + int length = ((byte[]) miscValue).length; + retValue = new byte[length]; + System.arraycopy(miscValue, 0, retValue, 0, length); + break; + default: + throw new ClassCastException( + "Invalid element type for this method: " + valueType); + } + return retValue; + } +} // end of class 'DataElement' definition diff --git a/java/midp/javax/bluetooth/DeviceClass.java b/java/midp/javax/bluetooth/DeviceClass.java new file mode 100644 index 000000000..667d30b2c --- /dev/null +++ b/java/midp/javax/bluetooth/DeviceClass.java @@ -0,0 +1,79 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.bluetooth; + +/* + * This class is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public class DeviceClass { + + // JAVADOC COMMENT ELIDED + private int record; + + // JAVADOC COMMENT ELIDED + private static final int MASK_MINOR = 0xFC; + + // JAVADOC COMMENT ELIDED + private static final int MASK_MAJOR = 0x1F00; + + // JAVADOC COMMENT ELIDED + private static final int MASK_SERVICE = 0xFFE000; + + // JAVADOC COMMENT ELIDED + private static final int MASK_OVERFLOW = 0xFF000000; + + // JAVADOC COMMENT ELIDED + public DeviceClass(int record) { + if ((record & MASK_OVERFLOW) != 0) { + throw new IllegalArgumentException( + "The 'record' bits out of (0-23) range."); + } + this.record = record; + } + + // JAVADOC COMMENT ELIDED + public int getServiceClasses() { + return record & MASK_SERVICE; + } + + // JAVADOC COMMENT ELIDED + public int getMajorDeviceClass() { + return record & MASK_MAJOR; + } + + // JAVADOC COMMENT ELIDED + public int getMinorDeviceClass() { + return record & MASK_MINOR; + } +} // end of class 'DeviceClass' definition diff --git a/java/midp/javax/bluetooth/DiscoveryAgent.java b/java/midp/javax/bluetooth/DiscoveryAgent.java new file mode 100644 index 000000000..f469194f5 --- /dev/null +++ b/java/midp/javax/bluetooth/DiscoveryAgent.java @@ -0,0 +1,98 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.bluetooth; +import com.sun.jsr082.bluetooth.DiscoveryAgentImpl; + +/* + * This class is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public class DiscoveryAgent { + + // JAVADOC COMMENT ELIDED + public static final int NOT_DISCOVERABLE = 0; + + // JAVADOC COMMENT ELIDED + public static final int GIAC = 0x9E8B33; + + // JAVADOC COMMENT ELIDED + public static final int LIAC = 0x9E8B00; + + // JAVADOC COMMENT ELIDED + public static final int CACHED = 0x00; + + // JAVADOC COMMENT ELIDED + public static final int PREKNOWN = 0x01; + + // JAVADOC COMMENT ELIDED + private DiscoveryAgentImpl discoveryAgentImpl; + + // JAVADOC COMMENT ELIDED + DiscoveryAgent() { + discoveryAgentImpl = DiscoveryAgentImpl.getInstance(); + } + + // JAVADOC COMMENT ELIDED + public RemoteDevice[] retrieveDevices(int option) { + return discoveryAgentImpl.retrieveDevices(option); + } + + // JAVADOC COMMENT ELIDED + public boolean startInquiry(int accessCode, DiscoveryListener listener) + throws BluetoothStateException { + return discoveryAgentImpl.startInquiry(accessCode, listener); + } + + // JAVADOC COMMENT ELIDED + public boolean cancelInquiry(DiscoveryListener listener) { + return discoveryAgentImpl.cancelInquiry(listener); + } + + // JAVADOC COMMENT ELIDED + public int searchServices(int[] attrSet, UUID[] uuidSet, RemoteDevice btDev, + DiscoveryListener discListener) throws BluetoothStateException { + return discoveryAgentImpl.searchServices(attrSet, uuidSet, btDev, + discListener); + } + + // JAVADOC COMMENT ELIDED + public boolean cancelServiceSearch(int transID) { + return discoveryAgentImpl.cancelServiceSearch(transID); + } + + // JAVADOC COMMENT ELIDED + public String selectService(UUID uuid, int security, boolean master) + throws BluetoothStateException { + return discoveryAgentImpl.selectService(uuid, security, master); + } +} // end of class 'DiscoveryAgent' definition diff --git a/java/midp/javax/bluetooth/DiscoveryListener.java b/java/midp/javax/bluetooth/DiscoveryListener.java new file mode 100644 index 000000000..50472e7df --- /dev/null +++ b/java/midp/javax/bluetooth/DiscoveryListener.java @@ -0,0 +1,76 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.bluetooth; + +/* + * This interface is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public interface DiscoveryListener { + + // JAVADOC COMMENT ELIDED + public static final int INQUIRY_COMPLETED = 0x00; + + // JAVADOC COMMENT ELIDED + public static final int INQUIRY_TERMINATED = 0x05; + + // JAVADOC COMMENT ELIDED + public static final int INQUIRY_ERROR = 0x07; + + // JAVADOC COMMENT ELIDED + public static final int SERVICE_SEARCH_COMPLETED = 0x01; + + // JAVADOC COMMENT ELIDED + public static final int SERVICE_SEARCH_TERMINATED = 0x02; + + // JAVADOC COMMENT ELIDED + public static final int SERVICE_SEARCH_ERROR = 0x03; + + // JAVADOC COMMENT ELIDED + public static final int SERVICE_SEARCH_NO_RECORDS = 0x04; + + // JAVADOC COMMENT ELIDED + public static final int SERVICE_SEARCH_DEVICE_NOT_REACHABLE = 0x06; + + // JAVADOC COMMENT ELIDED + public void deviceDiscovered(RemoteDevice btDevice, DeviceClass cod); + + // JAVADOC COMMENT ELIDED + public void servicesDiscovered(int transID, ServiceRecord[] servRecord); + + // JAVADOC COMMENT ELIDED + public void serviceSearchCompleted(int transID, int respCode); + + // JAVADOC COMMENT ELIDED + public void inquiryCompleted(int discType); +} // end of class 'DiscoveryListener' definition diff --git a/java/midp/javax/bluetooth/L2CAPConnection.java b/java/midp/javax/bluetooth/L2CAPConnection.java new file mode 100644 index 000000000..5e61f4641 --- /dev/null +++ b/java/midp/javax/bluetooth/L2CAPConnection.java @@ -0,0 +1,64 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.bluetooth; +import javax.microedition.io.Connection; +import java.io.IOException; + + +/* + * This interface is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public interface L2CAPConnection extends Connection { + + // JAVADOC COMMENT ELIDED + public static final int DEFAULT_MTU = 672; + + // JAVADOC COMMENT ELIDED + public static final int MINIMUM_MTU = 48; + + // JAVADOC COMMENT ELIDED + public int getTransmitMTU() throws IOException; + + // JAVADOC COMMENT ELIDED + public int getReceiveMTU() throws IOException; + + // JAVADOC COMMENT ELIDED + public void send(byte[] data) throws IOException; + + // JAVADOC COMMENT ELIDED + public int receive(byte[] inBuf) throws IOException; + + // JAVADOC COMMENT ELIDED + public boolean ready() throws IOException; +} // end of class 'L2CAPConnection' definition diff --git a/java/midp/javax/microedition/io/file/ConnectionClosedException.java b/java/midp/javax/bluetooth/L2CAPConnectionNotifier.java similarity index 66% rename from java/midp/javax/microedition/io/file/ConnectionClosedException.java rename to java/midp/javax/bluetooth/L2CAPConnectionNotifier.java index cd7f4537a..0dd6aeebf 100644 --- a/java/midp/javax/microedition/io/file/ConnectionClosedException.java +++ b/java/midp/javax/bluetooth/L2CAPConnectionNotifier.java @@ -1,5 +1,5 @@ /* - * + * * * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights * Reserved. Use is subject to license terms. @@ -24,26 +24,23 @@ * Clara, CA 95054 or visit www.sun.com if you need additional * information or have any questions. */ - + /* - * Copyright (C) 2002-2003 PalmSource, Inc. All Rights Reserved. + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. */ +package javax.bluetooth; +import javax.microedition.io.Connection; +import java.io.IOException; -package javax.microedition.io.file; -/** - * This class is defined by the JSR-75 specification - * PDA Optional Packages for the J2ME™ Platform +/* + * This interface is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. */ -// JAVADOC COMMENT ELIDED -public class ConnectionClosedException extends java.lang.RuntimeException { - // JAVADOC COMMENT ELIDED - public ConnectionClosedException() { - super(); - } +// JAVADOC COMMENT ELIDED +public interface L2CAPConnectionNotifier extends Connection { - // JAVADOC COMMENT ELIDED - public ConnectionClosedException(String detailMessage) { - super(detailMessage); - } -} + // JAVADOC COMMENT ELIDED + public L2CAPConnection acceptAndOpen() throws IOException; +} // end of class 'L2CAPConnectionNotifier' definition diff --git a/java/midp/javax/bluetooth/LocalDevice.java b/java/midp/javax/bluetooth/LocalDevice.java new file mode 100644 index 000000000..4dfaa68aa --- /dev/null +++ b/java/midp/javax/bluetooth/LocalDevice.java @@ -0,0 +1,175 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.bluetooth; +import com.sun.jsr082.bluetooth.LocalDeviceImpl; +import javax.microedition.io.Connection; + +/* + * This class is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public class LocalDevice { + + /* Keeps this singleton object. */ + private static LocalDevice localDevice; + + static { + try { + localDevice = getLocalDevice(); + } catch (BluetoothStateException e) { + throw new RuntimeException(e.getMessage()); + } + } + + /* Keeps the reference to implementation object. */ + private static LocalDeviceImpl localDeviceImpl; + + /* + * Keeps the discovery agen reference - + * because the DiscoveryAgent. is package private, + * so it can't be created ffrom the implemetation. + */ + private DiscoveryAgent discoveryAgent; + + /* + * The default constructor is hidden so that no one can create a new + * instance of the LocalDevice. To get the LocalDevice + * object for this device, use the getLocalDevice() + * static method in this class. + * + * @see #getLocalDevice + */ + private LocalDevice() {} + + // JAVADOC COMMENT ELIDED + public static LocalDevice getLocalDevice() throws BluetoothStateException { + + /* + * The method is not declared as synchronized to keep + * its signature unchanged. + */ + synchronized (LocalDevice.class) { + if (localDevice == null) { + try { + // create a shared impl object and 'this' + localDevice = new LocalDevice(); + + /* + * create a DiscoveryAgent from here. + * This should be done one time only + * regardless whether or not the system is + * initialized for the first time. + * + * We suppose the getLocalDevice() may be called + * for the next time if the first try failed. + */ + if (localDevice.discoveryAgent == null) { + localDevice.discoveryAgent = new DiscoveryAgent(); + } + + /* + * Constructing LocaldeviceImpl causes initialization + * of device properties and attributes. + */ + localDeviceImpl = LocalDeviceImpl.getInstance(); + } catch (BluetoothStateException bse) { + localDevice = null; + throw bse; + } catch (Throwable e) { + localDevice = null; + throw new BluetoothStateException(e.toString()); + } + } + } + return localDevice; + } + + // JAVADOC COMMENT ELIDED + public DiscoveryAgent getDiscoveryAgent() { + + /* + * This is an only exception for the "API/IMPL wrapper" + * scheme, i.e. the DiscoveryAgent object is stored + * locally in this class. + */ + return discoveryAgent; + } + + // JAVADOC COMMENT ELIDED + public String getFriendlyName() { + return localDeviceImpl.getFriendlyName(); + } + + // JAVADOC COMMENT ELIDED + public DeviceClass getDeviceClass() { + return localDeviceImpl.getDeviceClass(); + } + + // JAVADOC COMMENT ELIDED + public static String getProperty(String property) { + return localDevice != null ? localDeviceImpl.getProperty(property) : + null; + } + + // JAVADOC COMMENT ELIDED + public int getDiscoverable() { + return localDeviceImpl.getDiscoverable(); + } + + // JAVADOC COMMENT ELIDED + public String getBluetoothAddress() { + return localDeviceImpl.getBluetoothAddress(); + } + + // JAVADOC COMMENT ELIDED + public boolean setDiscoverable(int mode) throws BluetoothStateException { + return localDeviceImpl.setDiscoverable(mode); + } + + // JAVADOC COMMENT ELIDED + public ServiceRecord getRecord(Connection notifier) { + return localDeviceImpl.getRecord(notifier); + } + + // JAVADOC COMMENT ELIDED + public void updateRecord(ServiceRecord srvRecord) + throws ServiceRegistrationException { + localDeviceImpl.updateRecord(srvRecord); + } + + // JAVADOC COMMENT ELIDED + public static boolean isPowerOn() { + return localDeviceImpl.isPowerOn(); + } + +} // end of class 'LocalDevice' definition diff --git a/java/midp/javax/bluetooth/RemoteDevice.java b/java/midp/javax/bluetooth/RemoteDevice.java new file mode 100644 index 000000000..47837f1bd --- /dev/null +++ b/java/midp/javax/bluetooth/RemoteDevice.java @@ -0,0 +1,195 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.bluetooth; + +import java.io.IOException; +import javax.microedition.io.Connection; +import com.sun.jsr082.bluetooth.BluetoothConnection; +import com.sun.jsr082.bluetooth.BCC; + +/* + * This class is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public class RemoteDevice { + // JAVADOC COMMENT ELIDED + private long l_address; + + // JAVADOC COMMENT ELIDED + private String s_address; + + // JAVADOC COMMENT ELIDED + private String friendlyName; + + // JAVADOC COMMENT ELIDED + protected RemoteDevice(String address) { + if (address == null) { + throw new NullPointerException("null address"); + } + final String errorMsg = "Malformed address: " + address; + + if (address.length() != 12) { + throw new IllegalArgumentException(errorMsg); + } + + if (address.startsWith("-")) { + throw new IllegalArgumentException(errorMsg); + } + + try { + l_address = Long.parseLong(address, 16); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(errorMsg); + } + + // should be upper case only + address = address.toUpperCase(); + + try { + String lAddr = LocalDevice.getLocalDevice().getBluetoothAddress(); + + if (address.equals(lAddr)) { + throw new IllegalArgumentException( + "Can't use the local address."); + } + } catch (BluetoothStateException e) { + throw new RuntimeException("Can't initialize bluetooth support"); + } + s_address = address; + } + + // JAVADOC COMMENT ELIDED + public boolean isTrustedDevice() { + return BCC.getInstance().isTrusted(getBluetoothAddress()); + } + + // JAVADOC COMMENT ELIDED + public String getFriendlyName(boolean alwaysAsk) throws IOException { + // contact the remote device if name is not known or alwaysAsk is true + if (friendlyName == null || alwaysAsk) { + friendlyName = BCC.getInstance().getFriendlyName( + getBluetoothAddress()); + } + return friendlyName; + } + + // JAVADOC COMMENT ELIDED + public final String getBluetoothAddress() { + return s_address; + } + + // JAVADOC COMMENT ELIDED + public boolean equals(Object obj) { + return obj instanceof RemoteDevice && + l_address == ((RemoteDevice) obj).l_address; + } + + // JAVADOC COMMENT ELIDED + public int hashCode() { + return (int) ((l_address >>> 24) ^ (l_address & 0xffffffL)); + } + + // JAVADOC COMMENT ELIDED + public static RemoteDevice getRemoteDevice(Connection conn) + throws IOException { + return BluetoothConnection.getConnection(conn).getRemoteDevice(); + } + + // JAVADOC COMMENT ELIDED + public boolean authenticate() throws IOException { + if (!BCC.getInstance().isConnected(getBluetoothAddress())) { + throw new IOException("There are no open connections between the " + + "local device and this RemoteDevice."); + } + + return BCC.getInstance().authenticate(getBluetoothAddress()); + } + + // JAVADOC COMMENT ELIDED + public boolean authorize(Connection conn) throws IOException { + BluetoothConnection btconn = BluetoothConnection.getConnection(conn); + + if (!equals(btconn.getRemoteDevice())) { + throw new IllegalArgumentException("The specified connection " + + "is not a connection to this RemoteDevice."); + } + if (!btconn.isServerSide()) { + throw new IllegalArgumentException("The local device is client " + + "rather than the server for the specified connection."); + } + + return authenticate() && (isTrustedDevice() || btconn.isAuthorized() || + btconn.authorize()); + } + + // JAVADOC COMMENT ELIDED + public boolean encrypt(Connection conn, boolean on) throws IOException { + BluetoothConnection btconn = BluetoothConnection.getConnection(conn); + if (!equals(btconn.getRemoteDevice())) { + throw new IllegalArgumentException("The specified connection " + + "is not a connection to this RemoteDevice."); + } + if (on && !authenticate()) { + return false; + } + return btconn.encrypt(on); + } + + // JAVADOC COMMENT ELIDED + public boolean isAuthenticated() { + return BCC.getInstance().isAuthenticated(getBluetoothAddress()); + } + + // JAVADOC COMMENT ELIDED + public boolean isAuthorized(Connection conn) throws IOException { + BluetoothConnection btconn = BluetoothConnection.getConnection(conn); + RemoteDevice device; + + try { + device = btconn.getRemoteDevice(); + } catch (IllegalArgumentException e) { + return false; + } + if (!equals(device)) { + throw new IllegalArgumentException("The specified connection " + + "is not a connection to this RemoteDevice."); + } + + return btconn.isServerSide() && btconn.isAuthorized(); + } + + // JAVADOC COMMENT ELIDED + public boolean isEncrypted() { + return BCC.getInstance().isEncrypted(getBluetoothAddress()); + } +} // end of class 'RemoteDevice' definition diff --git a/java/midp/javax/bluetooth/ServiceRecord.java b/java/midp/javax/bluetooth/ServiceRecord.java new file mode 100644 index 000000000..df9e8696a --- /dev/null +++ b/java/midp/javax/bluetooth/ServiceRecord.java @@ -0,0 +1,72 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.bluetooth; +import java.io.IOException; + +/* + * This interface is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public interface ServiceRecord { + + // JAVADOC COMMENT ELIDED + public static final int NOAUTHENTICATE_NOENCRYPT = 0; + + // JAVADOC COMMENT ELIDED + public static final int AUTHENTICATE_NOENCRYPT = 0x01; + + // JAVADOC COMMENT ELIDED + public static final int AUTHENTICATE_ENCRYPT = 0x02; + + // JAVADOC COMMENT ELIDED + public DataElement getAttributeValue(int attrID); + + // JAVADOC COMMENT ELIDED + public RemoteDevice getHostDevice(); + + // JAVADOC COMMENT ELIDED + public int[] getAttributeIDs(); + + // JAVADOC COMMENT ELIDED + public boolean populateRecord(int[] attrIDs) + throws IOException; + + // JAVADOC COMMENT ELIDED + public String getConnectionURL(int requiredSecurity, boolean mustBeMaster); + + // JAVADOC COMMENT ELIDED + public void setDeviceServiceClasses(int classes); + + // JAVADOC COMMENT ELIDED + public boolean setAttributeValue(int attrID, DataElement attrValue); +} // end of class 'ServiceRecord' definition diff --git a/java/midp/javax/bluetooth/ServiceRegistrationException.java b/java/midp/javax/bluetooth/ServiceRegistrationException.java new file mode 100644 index 000000000..79606c6df --- /dev/null +++ b/java/midp/javax/bluetooth/ServiceRegistrationException.java @@ -0,0 +1,51 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.bluetooth; +import java.io.IOException; + +/* + * This class is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public class ServiceRegistrationException extends IOException { + + // JAVADOC COMMENT ELIDED + public ServiceRegistrationException() { + super(); + } + + // JAVADOC COMMENT ELIDED + public ServiceRegistrationException(String msg) { + super(msg); + } +} // end of class 'ServiceRegistrationException' definition diff --git a/java/midp/javax/bluetooth/UUID.java b/java/midp/javax/bluetooth/UUID.java new file mode 100644 index 000000000..975cd5b47 --- /dev/null +++ b/java/midp/javax/bluetooth/UUID.java @@ -0,0 +1,225 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.bluetooth; + +/* + * This class is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public class UUID { + + // JAVADOC COMMENT ELIDED + private long highBits; + + // JAVADOC COMMENT ELIDED + private long lowBits; + + // JAVADOC COMMENT ELIDED + private static final long BASE_UUID_HIGHT = 0x1000L; + + // JAVADOC COMMENT ELIDED + private static final long BASE_UUID_LOW = 0x800000805F9B34FBL; + + // JAVADOC COMMENT ELIDED + private static final char[] digits = { + '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'A', 'B', + 'C', 'D', 'E', 'F' + }; + + // JAVADOC COMMENT ELIDED + public UUID(long uuidValue) { + + // check the specified value is out of range + if (uuidValue < 0 || uuidValue > 0xffffffffL) { + throw new IllegalArgumentException( + "The 'uuidValue' is out of [0, 2^32 - 1] range: " + + uuidValue); + } + + /* + * Create a UUID from 16/32 bits value. + * + * 128_bit_value = 16_bit_value * 2^96 + Bluetooth_Base_UUID + * 128_bit_value = 32_bit_value * 2^96 + Bluetooth_Base_UUID + * + * No need to check the "overflow/negative", because + * uuidValue is 32 bits & BASE_UUID_HIGHT is 16 bits. + */ + highBits = (uuidValue << 32) | BASE_UUID_HIGHT; + lowBits = BASE_UUID_LOW; + } + + // JAVADOC COMMENT ELIDED + public UUID(String uuidValue, boolean shortUUID) { + if (uuidValue == null) { + throw new NullPointerException("Specified 'uuidValue' is null"); + } + + /* + * The zero length is double checked by the parsing operation, + * but the NumberFormatException is thrown in that case - + * we need IllegalArgumentException according to spec. + */ + if (uuidValue.length() == 0 || (shortUUID && uuidValue.length() > 8) || + uuidValue.length() > 32) { + throw new IllegalArgumentException( + "Invalid length of specified 'uuidValue': " + + uuidValue.length()); + } + + // check if sign character presents + if (uuidValue.indexOf('-') != -1) { + throw new NumberFormatException( + "The '-' character is not allowed: " + uuidValue); + } + + /* + * 16-bit or 32-bit UUID case. + */ + if (shortUUID) { + + // this checks the format and may throw a NumberFormatException + long val = Long.parseLong(uuidValue, 16); + + /* + * create a UUID from 16/32 bits value. + * + * No need to check the "overflow/negative", because + * lVal is 32 bits & BASE_UUID_HIGHT is 16 bits. + */ + highBits = (val << 32) | BASE_UUID_HIGHT; + lowBits = BASE_UUID_LOW; + return; + } + + /* + * 128-bit UUID case. + */ + highBits = 0x0L; + + // simple case (optimization) + if (uuidValue.length() < 16) { + lowBits = Long.parseLong(uuidValue, 16); + return; + } + + /* + * We have to do a 32 bits parsing, because the + * Long.parseLong("ffff ffff ffff ffff") does not + * parse such an unsigned number. + */ + int l = uuidValue.length(); + lowBits = Long.parseLong(uuidValue.substring(l - 8), 16); + lowBits |= (Long.parseLong(uuidValue.substring(l - 16, l - 8), 16) + << 32); + + if (l == 16) { + return; + } + + if (l <= 24) { + highBits = Long.parseLong(uuidValue.substring(0, l - 16), 16); + } else { + highBits = Long.parseLong(uuidValue.substring(l - 24, l - 16), 16); + highBits |= (Long.parseLong(uuidValue.substring(0, l - 24), 16) + << 32); + } + } + + // JAVADOC COMMENT ELIDED + public String toString() { + + /* + * This implementation is taken from cldc1.1 Integer#toUnsignedString + * one. The implementation which uses Integer#toHexString() is + * 2-3 times slower, so such a code duplication is required here. + */ + int[] ints = new int[] { + (int) (lowBits & 0xffffffffL), + (int) (lowBits >>> 32 & 0xffffffffL), + (int) (highBits & 0xffffffffL), + (int) (highBits >>> 32 & 0xffffffffL) + }; + int charPos = 32; + char[] buf = new char[charPos]; + int shift = 4; + int mask = 0xf; + int needZerosIndex = -1; + + /* + * check with part of value requires the zero characters. + * + * I.e. the original algorithm gives as an 1 character + * for the value '1', but we may want 00000001. + */ + for (int i = 3; i >= 0; i--) { + if (ints[i] != 0) { + needZerosIndex = i - 1; + break; + } + } + + /* + * Process parts of UUID from low parts to high ones. + */ + for (int i = 0; i < ints.length; i++) { + + /* + * The 16 bits are zero & no need to fill with 0, + * and it's not a UUID with value '0' (i != 0). + */ + if (ints[i] == 0 && needZerosIndex < i && i != 0) { + continue; + } + + for (int j = 0; j < 8; j++) { + buf[--charPos] = digits[ints[i] & mask]; + ints[i] >>>= shift; + } + } + return new String(buf, charPos, (32 - charPos)); + } + + // JAVADOC COMMENT ELIDED + public boolean equals(Object value) { + return value instanceof UUID && + lowBits == ((UUID) value).lowBits && + highBits == ((UUID) value).highBits; + } + + // JAVADOC COMMENT ELIDED + public int hashCode() { + return (int) (highBits ^ highBits >> 32 ^ lowBits ^ lowBits >> 32); + } +} // end of class 'UUID' definition diff --git a/java/midp/javax/microedition/io/file/FileConnection.java b/java/midp/javax/microedition/io/file/FileConnection.java deleted file mode 100644 index ab2f9d026..000000000 --- a/java/midp/javax/microedition/io/file/FileConnection.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * - * - * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights - * Reserved. Use is subject to license terms. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License version - * 2 only, as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License version 2 for more details (a copy is - * included at /legal/license.txt). - * - * You should have received a copy of the GNU General Public License - * version 2 along with this work; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA - * - * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa - * Clara, CA 95054 or visit www.sun.com if you need additional - * information or have any questions. - */ - -/* - * Copyright (C) 2002-2003 PalmSource, Inc. All Rights Reserved. - */ - -package javax.microedition.io.file; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.InputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.util.Enumeration; - -/** - * This class is defined by the JSR-75 specification - * PDA Optional Packages for the J2ME™ Platform - */ -// JAVADOC COMMENT ELIDED -public interface FileConnection extends javax.microedition.io.StreamConnection { - - // JAVADOC COMMENT ELIDED - public boolean isOpen(); - - // JAVADOC COMMENT ELIDED - public InputStream openInputStream() throws IOException; - - // JAVADOC COMMENT ELIDED - public DataInputStream openDataInputStream() throws IOException; - - // JAVADOC COMMENT ELIDED - public OutputStream openOutputStream() throws IOException; - - // JAVADOC COMMENT ELIDED - public DataOutputStream openDataOutputStream() throws IOException; - - // JAVADOC COMMENT ELIDED - public OutputStream openOutputStream(long byteOffset) throws IOException; - - // JAVADOC COMMENT ELIDED - public long totalSize(); - - // JAVADOC COMMENT ELIDED - public long availableSize(); - - // JAVADOC COMMENT ELIDED - public long usedSize(); - - // JAVADOC COMMENT ELIDED - public long directorySize(boolean includeSubDirs) throws IOException; - - // JAVADOC COMMENT ELIDED - public long fileSize() throws IOException; - - // JAVADOC COMMENT ELIDED - public boolean canRead(); - - // JAVADOC COMMENT ELIDED - public boolean canWrite(); - - // JAVADOC COMMENT ELIDED - public boolean isHidden(); - - // JAVADOC COMMENT ELIDED - public void setReadable(boolean readable) throws IOException; - - // JAVADOC COMMENT ELIDED - public void setWritable(boolean writable)throws IOException; - - // JAVADOC COMMENT ELIDED - public void setHidden(boolean hidden) throws IOException; - - // JAVADOC COMMENT ELIDED - public Enumeration list() throws IOException; - - // JAVADOC COMMENT ELIDED - public Enumeration list(String filter, boolean includeHidden) - throws IOException; - - // JAVADOC COMMENT ELIDED - public void mkdir() throws IOException; - - // JAVADOC COMMENT ELIDED - public void create() throws IOException; - - // JAVADOC COMMENT ELIDED - public abstract boolean exists(); - - // JAVADOC COMMENT ELIDED - public boolean isDirectory(); - - // JAVADOC COMMENT ELIDED - public void delete() throws java.io.IOException; - - // JAVADOC COMMENT ELIDED - public abstract void rename(String newName) throws IOException; - - // JAVADOC COMMENT ELIDED - public abstract void truncate(long byteOffset) throws IOException; - - // JAVADOC COMMENT ELIDED - public abstract void setFileConnection(String fileName) throws IOException; - - // JAVADOC COMMENT ELIDED - public String getName(); - - // JAVADOC COMMENT ELIDED - public String getPath(); - - // JAVADOC COMMENT ELIDED - public String getURL(); - - // JAVADOC COMMENT ELIDED - public long lastModified(); -} diff --git a/java/midp/javax/obex/Authenticator.java b/java/midp/javax/obex/Authenticator.java new file mode 100644 index 000000000..c5906bc98 --- /dev/null +++ b/java/midp/javax/obex/Authenticator.java @@ -0,0 +1,47 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.obex; + +/* + * This interface is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public interface Authenticator { + + // JAVADOC COMMENT ELIDED + public PasswordAuthentication onAuthenticationChallenge(String description, + boolean isUserIdRequired, boolean isFullAccess); + + // JAVADOC COMMENT ELIDED + public byte[] onAuthenticationResponse(byte[] userName); +} diff --git a/java/midp/javax/obex/HeaderSet.java b/java/midp/javax/obex/HeaderSet.java new file mode 100644 index 000000000..f97a92877 --- /dev/null +++ b/java/midp/javax/obex/HeaderSet.java @@ -0,0 +1,93 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.obex; +import java.io.IOException; + +/* + * This interface is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public interface HeaderSet { + + // JAVADOC COMMENT ELIDED + public static final int COUNT = 0xC0; + + // JAVADOC COMMENT ELIDED + public static final int NAME = 0x01; + + // JAVADOC COMMENT ELIDED + public static final int TYPE = 0x42; + + // JAVADOC COMMENT ELIDED + public static final int LENGTH = 0xC3; + + // JAVADOC COMMENT ELIDED + public static final int TIME_ISO_8601 = 0x44; + + // JAVADOC COMMENT ELIDED + public static final int TIME_4_BYTE = 0xC4; + + // JAVADOC COMMENT ELIDED + public static final int DESCRIPTION = 0x05; + + // JAVADOC COMMENT ELIDED + public static final int TARGET = 0x46; + + // JAVADOC COMMENT ELIDED + public static final int HTTP = 0x47; + + // JAVADOC COMMENT ELIDED + public static final int WHO = 0x4A; + + // JAVADOC COMMENT ELIDED + public static final int OBJECT_CLASS = 0x4F; + + // JAVADOC COMMENT ELIDED + public static final int APPLICATION_PARAMETER = 0x4C; + + // JAVADOC COMMENT ELIDED + public void setHeader(int headerID, Object headerValue); + + // JAVADOC COMMENT ELIDED + public Object getHeader(int headerID) throws IOException; + + // JAVADOC COMMENT ELIDED + public int[] getHeaderList() throws IOException; + + // JAVADOC COMMENT ELIDED + public void createAuthenticationChallenge(String realm, boolean userID, + boolean access); + + // JAVADOC COMMENT ELIDED + public int getResponseCode() throws IOException; +} diff --git a/java/midp/javax/obex/Operation.java b/java/midp/javax/obex/Operation.java new file mode 100644 index 000000000..f5aded936 --- /dev/null +++ b/java/midp/javax/obex/Operation.java @@ -0,0 +1,54 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.obex; +import java.io.IOException; +import javax.microedition.io.ContentConnection; + +/* + * This interface is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public interface Operation extends ContentConnection { + + // JAVADOC COMMENT ELIDED + public void abort() throws IOException; + + // JAVADOC COMMENT ELIDED + public HeaderSet getReceivedHeaders() throws IOException; + + // JAVADOC COMMENT ELIDED + public void sendHeaders(HeaderSet headers) throws IOException; + + // JAVADOC COMMENT ELIDED + public int getResponseCode() throws IOException; +} diff --git a/java/midp/javax/obex/PasswordAuthentication.java b/java/midp/javax/obex/PasswordAuthentication.java new file mode 100644 index 000000000..3063e53ee --- /dev/null +++ b/java/midp/javax/obex/PasswordAuthentication.java @@ -0,0 +1,63 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.obex; + +/* + * This class is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public class PasswordAuthentication { + // JAVADOC COMMENT ELIDED + private byte[] password; + // JAVADOC COMMENT ELIDED + private byte[] userName; + + // JAVADOC COMMENT ELIDED + public PasswordAuthentication(byte[] userName, byte[] password) { + if (password == null) { + throw new NullPointerException("null password"); + } + this.password = password; + this.userName = userName; + } + + // JAVADOC COMMENT ELIDED + public byte[] getUserName() { + return userName; + } + + // JAVADOC COMMENT ELIDED + public byte[] getPassword() { + return password; + } +} diff --git a/java/midp/javax/obex/ResponseCodes.java b/java/midp/javax/obex/ResponseCodes.java new file mode 100644 index 000000000..ac63a35f9 --- /dev/null +++ b/java/midp/javax/obex/ResponseCodes.java @@ -0,0 +1,154 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.obex; + +/* + * This class is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public class ResponseCodes { + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_OK = 0xA0; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_CREATED = 0xA1; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_ACCEPTED = 0xA2; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_NOT_AUTHORITATIVE = 0xA3; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_NO_CONTENT = 0xA4; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_RESET = 0xA5; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_PARTIAL = 0xA6; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_MULT_CHOICE = 0xB0; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_MOVED_PERM = 0xB1; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_MOVED_TEMP = 0xB2; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_SEE_OTHER = 0xB3; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_NOT_MODIFIED = 0xB4; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_USE_PROXY = 0xB5; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_BAD_REQUEST = 0xC0; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_UNAUTHORIZED = 0xC1; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_PAYMENT_REQUIRED = 0xC2; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_FORBIDDEN = 0xC3; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_NOT_FOUND = 0xC4; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_BAD_METHOD = 0xC5; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_NOT_ACCEPTABLE = 0xC6; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_PROXY_AUTH = 0xC7; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_TIMEOUT = 0xC8; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_CONFLICT = 0xC9; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_GONE = 0xCA; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_LENGTH_REQUIRED = 0xCB; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_PRECON_FAILED = 0xCC; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_ENTITY_TOO_LARGE = 0xCD; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_REQ_TOO_LARGE = 0xCE; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_UNSUPPORTED_TYPE = 0xCF; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_INTERNAL_ERROR = 0xD0; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_NOT_IMPLEMENTED = 0xD1; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_BAD_GATEWAY = 0xD2; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_UNAVAILABLE = 0xD3; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_GATEWAY_TIMEOUT = 0xD4; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_HTTP_VERSION = 0xD5; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_DATABASE_FULL = 0xE0; + + // JAVADOC COMMENT ELIDED + public static final int OBEX_DATABASE_LOCKED = 0xE1; + + // JAVADOC COMMENT ELIDED + private ResponseCodes() {} +} diff --git a/java/midp/javax/obex/ServerRequestHandler.java b/java/midp/javax/obex/ServerRequestHandler.java new file mode 100644 index 000000000..1bf773e48 --- /dev/null +++ b/java/midp/javax/obex/ServerRequestHandler.java @@ -0,0 +1,103 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.obex; +import com.sun.jsr082.obex.HeaderSetImpl; + +/* + * This class is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public class ServerRequestHandler { + + // JAVADOC COMMENT ELIDED + private long connId; + + // JAVADOC COMMENT ELIDED + protected ServerRequestHandler() { + connId = -1; + } + + // JAVADOC COMMENT ELIDED + public final HeaderSet createHeaderSet() { + return new HeaderSetImpl(HeaderSetImpl.OWNER_SERVER_USER); + } + + // JAVADOC COMMENT ELIDED + public void setConnectionID(long id) { + if (id < -1L || id > 0xFFFFFFFFL) { + throw new IllegalArgumentException("invalid id"); + } + connId = id; + } + + // JAVADOC COMMENT ELIDED + public long getConnectionID() { + return connId; + } + + // JAVADOC COMMENT ELIDED + public int onConnect(HeaderSet request, HeaderSet reply) { + return ResponseCodes.OBEX_HTTP_OK; + } + + // JAVADOC COMMENT ELIDED + public void onDisconnect(HeaderSet request, HeaderSet reply) { + // do nothing + } + + // JAVADOC COMMENT ELIDED + public int onSetPath(HeaderSet request, HeaderSet reply, boolean backup, + boolean create) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + // JAVADOC COMMENT ELIDED + public int onDelete(HeaderSet request, HeaderSet reply) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + // JAVADOC COMMENT ELIDED + public int onPut(Operation op) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + // JAVADOC COMMENT ELIDED + public int onGet(Operation op) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + // JAVADOC COMMENT ELIDED + public void onAuthenticationFailure(byte[] userName) { + // do nothing + } +} diff --git a/java/midp/javax/obex/SessionNotifier.java b/java/midp/javax/obex/SessionNotifier.java new file mode 100644 index 000000000..fa269d25b --- /dev/null +++ b/java/midp/javax/obex/SessionNotifier.java @@ -0,0 +1,50 @@ +/* + * + * + * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights + * Reserved. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 only, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License version 2 for more details (a copy is + * included at /legal/license.txt). + * + * You should have received a copy of the GNU General Public License + * version 2 along with this work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa + * Clara, CA 95054 or visit www.sun.com if you need additional + * information or have any questions. + */ + +/* + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + */ +package javax.obex; +import java.io.IOException; +import javax.microedition.io.Connection; + +/* + * This interface is defined by the JSR-82 specification + * Java™ APIs for Bluetooth™ Wireless Technology, + * Version 1.1. + */ +// JAVADOC COMMENT ELIDED +public interface SessionNotifier extends Connection { + + // JAVADOC COMMENT ELIDED + public Connection acceptAndOpen(ServerRequestHandler handler) + throws IOException; + + // JAVADOC COMMENT ELIDED + public Connection acceptAndOpen(ServerRequestHandler handler, + Authenticator auth) throws IOException; +} diff --git a/legacy.js b/legacy.js index 96e3ae83e..a05b49365 100644 --- a/legacy.js +++ b/legacy.js @@ -18,3 +18,49 @@ if (!String.prototype.contains) { return String.prototype.indexOf.apply(this, arguments) !== -1; }; } + +if (!Array.prototype.find) { + Array.prototype.find = function(predicate) { + if (this == null) { + throw new TypeError('Array.prototype.find called on null or undefined'); + } + if (typeof predicate !== 'function') { + throw new TypeError('predicate must be a function'); + } + var list = Object(this); + var length = list.length >>> 0; + var thisArg = arguments[1]; + var value; + + for (var i = 0; i < length; i++) { + value = list[i]; + if (predicate.call(thisArg, value, i, list)) { + return value; + } + } + return undefined; + }; +} + +if (!Array.prototype.findIndex) { + Array.prototype.findIndex = function(predicate) { + if (this == null) { + throw new TypeError('Array.prototype.find called on null or undefined'); + } + if (typeof predicate !== 'function') { + throw new TypeError('predicate must be a function'); + } + var list = Object(this); + var length = list.length >>> 0; + var thisArg = arguments[1]; + var value; + + for (var i = 0; i < length; i++) { + value = list[i]; + if (predicate.call(thisArg, value, i, list)) { + return i; + } + } + return -1; + }; +} diff --git a/libs/async_storage.js b/libs/async_storage.js deleted file mode 100644 index 2282ae9ad..000000000 --- a/libs/async_storage.js +++ /dev/null @@ -1,179 +0,0 @@ -'use strict'; - -/** - * This module defines an asynchronous version of the localStorage API, backed by - * an IndexedDB database. It creates a global asyncStorage object that has - * methods like the localStorage object. - * - * To store a value use setItem: - * - * asyncStorage.setItem('key', 'value'); - * - * If you want confirmation that the value has been stored, pass a callback - * function as the third argument: - * - * asyncStorage.setItem('key', 'newvalue', function() { - * console.log('new value stored'); - * }); - * - * To read a value, call getItem(), but note that you must supply a callback - * function that the value will be passed to asynchronously: - * - * asyncStorage.getItem('key', function(value) { - * console.log('The value of key is:', value); - * }); - * - * Note that unlike localStorage, asyncStorage does not allow you to store and - * retrieve values by setting and querying properties directly. You cannot just - * write asyncStorage.key; you have to explicitly call setItem() or getItem(). - * - * removeItem(), clear(), length(), and key() are like the same-named methods of - * localStorage, but, like getItem() and setItem() they take a callback - * argument. - * - * The asynchronous nature of getItem() makes it tricky to retrieve multiple - * values. But unlike localStorage, asyncStorage does not require the values you - * store to be strings. So if you need to save multiple values and want to - * retrieve them together, in a single asynchronous operation, just group the - * values into a single object. The properties of this object may not include - * DOM elements, but they may include things like Blobs and typed arrays. - */ - -var asyncStorage = (function() { - var indexedDB = window.indexedDB || window.webkitIndexedDB || - window.mozIndexedDB || window.msIndexedDB; - - var DBNAME = 'asyncStorage'; - var DBVERSION = 1; - var STORENAME = 'keyvaluepairs'; - var db = null; - - function withDatabase(f) { - if (db) { - f(); - } else { - var openreq = indexedDB.open(DBNAME, DBVERSION); - openreq.onerror = function withStoreOnError() { - console.error('asyncStorage: can\'t open database:', - openreq.error.name); - }; - openreq.onupgradeneeded = function withStoreOnUpgradeNeeded() { - // First time setup: create an empty object store - openreq.result.createObjectStore(STORENAME); - }; - openreq.onsuccess = function withStoreOnSuccess() { - db = openreq.result; - f(); - }; - } - } - - function withStore(type, callback, oncomplete) { - withDatabase(function() { - var transaction = db.transaction(STORENAME, type); - if (oncomplete) { - transaction.oncomplete = oncomplete; - } - callback(transaction.objectStore(STORENAME)); - }); - } - - function getItem(key, callback) { - var req; - withStore('readonly', function getItemBody(store) { - req = store.get(key); - req.onerror = function getItemOnError() { - console.error('Error in asyncStorage.getItem(): ', req.error.name); - }; - }, function onComplete() { - var value = req.result; - if (value === undefined) { - value = null; - } - callback(value); - }); - } - - function setItem(key, value, callback) { - withStore('readwrite', function setItemBody(store) { - var req = store.put(value, key); - req.onerror = function setItemOnError() { - console.error('Error in asyncStorage.setItem(): ', req.error.name); - }; - }, callback); - } - - function removeItem(key, callback) { - withStore('readwrite', function removeItemBody(store) { - var req = store.delete(key); - req.onerror = function removeItemOnError() { - console.error('Error in asyncStorage.removeItem(): ', req.error.name); - }; - }, callback); - } - - function clear(callback) { - withStore('readwrite', function clearBody(store) { - var req = store.clear(); - req.onerror = function clearOnError() { - console.error('Error in asyncStorage.clear(): ', req.error.name); - }; - }, callback); - } - - function length(callback) { - var req; - withStore('readonly', function lengthBody(store) { - req = store.count(); - req.onerror = function lengthOnError() { - console.error('Error in asyncStorage.length(): ', req.error.name); - }; - }, function onComplete() { - callback(req.result); - }); - } - - function key(n, callback) { - if (n < 0) { - callback(null); - return; - } - - var req; - withStore('readonly', function keyBody(store) { - var advanced = false; - req = store.openCursor(); - req.onsuccess = function keyOnSuccess() { - var cursor = req.result; - if (!cursor) { - // this means there weren't enough keys - return; - } - if (n === 0 || advanced) { - // Either 1) we have the first key, return it if that's what they - // wanted, or 2) we've got the nth key. - return; - } - - // Otherwise, ask the cursor to skip ahead n records - advanced = true; - cursor.advance(n); - }; - req.onerror = function keyOnError() { - console.error('Error in asyncStorage.key(): ', req.error.name); - }; - }, function onComplete() { - var cursor = req.result; - callback(cursor ? cursor.key : null); - }); - } - - return { - getItem: getItem, - setItem: setItem, - removeItem: removeItem, - clear: clear, - length: length, - key: key - }; -})(); diff --git a/libs/fs.js b/libs/fs.js index adb572041..03655dc42 100644 --- a/libs/fs.js +++ b/libs/fs.js @@ -1,6 +1,88 @@ 'use strict'; +var DEBUG_FS = false; + var fs = (function() { + var Store = function() { + this.map = new Map(); + this.db = null; + }; + + Store.DBNAME = "asyncStorage"; + Store.DBVERSION = 1; + Store.DBSTORENAME = "keyvaluepairs"; + + Store.prototype.init = function(cb) { + var openreq = indexedDB.open(Store.DBNAME, Store.DBVERSION); + openreq.onerror = function() { + console.error("error opening database: " + openreq.error.name); + }; + openreq.onupgradeneeded = function() { + openreq.result.createObjectStore(Store.DBSTORENAME); + }; + openreq.onsuccess = (function() { + this.db = openreq.result; + cb(); + }).bind(this); + }; + + Store.prototype.getItem = function(key, cb) { + if (this.map.has(key)) { + var value = this.map.get(key); + window.setZeroTimeout(function() { cb(value) }); + } else { + var transaction = this.db.transaction(Store.DBSTORENAME, "readonly"); + var objectStore = transaction.objectStore(Store.DBSTORENAME); + var req = objectStore.get(key); + req.onerror = function() { + console.error("Error getting " + key + ": " + req.error.name); + }; + transaction.oncomplete = (function() { + var value = req.result; + if (value === undefined) { + value = null; + } + this.map.set(key, value); + cb(value); + }).bind(this); + } + }; + + Store.prototype.setItem = function(key, value) { + this.map.set(key, value); + + var transaction = this.db.transaction(Store.DBSTORENAME, "readwrite"); + var objectStore = transaction.objectStore(Store.DBSTORENAME); + var req = objectStore.put(value, key); + req.onerror = function() { + console.error("Error putting " + key + ": " + req.error.name); + }; + }; + + Store.prototype.removeItem = function(key) { + this.map.delete(key); + + var transaction = this.db.transaction(Store.DBSTORENAME, "readwrite"); + var objectStore = transaction.objectStore(Store.DBSTORENAME); + var req = objectStore.delete(key); + req.onerror = function() { + console.error("Error deleting " + key + ": " + req.error.name); + }; + }; + + Store.prototype.clear = function() { + this.map.clear(); + + var transaction = this.db.transaction(Store.DBSTORENAME, "readwrite"); + var objectStore = transaction.objectStore(Store.DBSTORENAME); + var req = objectStore.clear(); + req.onerror = function() { + console.error("Error clearing store: " + req.error.name); + }; + } + + var store = new Store(); + var FileBuffer = function(array) { this.array = array; this.contentSize = array.byteLength; @@ -75,25 +157,31 @@ var fs = (function() { return path.slice(path.lastIndexOf("/") + 1); } - function init(cb) { - asyncStorage.getItem("/", function(data) { + function initRootDir(cb) { + store.getItem("/", function(data) { if (data) { cb(); } else { - asyncStorage.setItem("/", [], function() { - setStat("/", { mtime: Date.now(), isDir: true }, cb); - }); + store.setItem("/", []); + setStat("/", { mtime: Date.now(), isDir: true }); + cb(); } }); } + function init(cb) { + store.init(function() { + initRootDir(cb || function() {}); + }); + } + var openedFiles = [null, null, null]; - var fileStats = {}; function open(path, cb) { path = normalizePath(path); + if (DEBUG_FS) { console.log("fs open " + path); } - asyncStorage.getItem(path, function(blob) { + store.getItem(path, function(blob) { if (blob == null || !(blob instanceof Blob)) { cb(-1); } else { @@ -112,20 +200,11 @@ var fs = (function() { }); } - function close(fd, cb) { + function close(fd) { if (fd >= 0 && openedFiles[fd]) { - flush(fd, function() { - // Replace descriptor object with null value instead of removing it from - // the array so we don't change the indexes of the other objects. - openedFiles.splice(fd, 1, null); - if (cb) { - cb(); - } - }); - } else { - if (cb) { - cb(); - } + if (DEBUG_FS) { console.log("fs close " + openedFiles[fd].path); } + flush(fd); + openedFiles.splice(fd, 1, null); } } @@ -133,6 +212,7 @@ var fs = (function() { if (!openedFiles[fd]) { return null; } + if (DEBUG_FS) { console.log("fs read " + openedFiles[fd].path); } var buffer = openedFiles[fd].buffer; @@ -153,6 +233,8 @@ var fs = (function() { } function write(fd, data, from) { + if (DEBUG_FS) { console.log("fs write " + openedFiles[fd].path); } + if (typeof from == "undefined") { from = openedFiles[fd].position; } @@ -173,7 +255,6 @@ var fs = (function() { file.position = from + data.byteLength; file.stat = { mtime: Date.now(), isDir: false, size: buffer.contentSize }; file.dirty = true; - fileStats[file.path] = file.stat; } function getpos(fd) { @@ -192,28 +273,47 @@ var fs = (function() { return openedFiles[fd].buffer.contentSize; } - function flush(fd, cb) { + function flush(fd) { + if (DEBUG_FS) { console.log("fs flush " + openedFiles[fd].path); } + + var openedFile = openedFiles[fd]; + // Bail early if the file has not been modified. - if (!openedFiles[fd].dirty) { - cb(); + if (!openedFile.dirty) { return; } - var blob = new Blob([openedFiles[fd].buffer.getContent()]); - asyncStorage.setItem(openedFiles[fd].path, blob, function() { - openedFiles[fd].dirty = false; - if (openedFiles[fd].stat) { - setStat(openedFiles[fd].path, openedFiles[fd].stat, cb); - } else { - cb(); + var blob = new Blob([openedFile.buffer.getContent()]); + store.setItem(openedFile.path, blob); + openedFile.dirty = false; + if (openedFile.stat) { + setStat(openedFile.path, openedFile.stat); + } + } + + function flushAll() { + for (var fd = 0; fd < openedFiles.length; fd++) { + if (!openedFiles[fd] || !openedFiles[fd].dirty) { + continue; } - }); + flush(fd); + } } + // Due to bug #227, we don't support Object::finalize(). But the Java + // filesystem implementation requires the `finalize` method to save cached + // file data if user doesn't flush or close the file explicitly. To avoid + // losing data, we flush files periodically. + setInterval(flushAll, 5000); + + // Flush files when app goes into background. + window.addEventListener("pagehide", flushAll); + function list(path, cb) { path = normalizePath(path); + if (DEBUG_FS) { console.log("fs list " + path); } - asyncStorage.getItem(path, function(files) { + store.getItem(path, function(files) { if (files == null || files instanceof Blob) { cb(null); } else { @@ -224,6 +324,7 @@ var fs = (function() { function exists(path, cb) { path = normalizePath(path); + if (DEBUG_FS) { console.log("fs exists " + path); } stat(path, function(stat) { cb(stat ? true : false); @@ -232,13 +333,13 @@ var fs = (function() { function truncate(path, cb) { path = normalizePath(path); + if (DEBUG_FS) { console.log("fs truncate " + path); } stat(path, function(stat) { if (stat && !stat.isDir) { - asyncStorage.setItem(path, new Blob(), function() { - setStat(path, { mtime: Date.now(), isDir: false, size: 0 }); - cb(true); - }); + store.setItem(path, new Blob()); + setStat(path, { mtime: Date.now(), isDir: false, size: 0 }); + cb(true); } else { cb(false); } @@ -246,19 +347,22 @@ var fs = (function() { } function ftruncate(fd, size) { + if (DEBUG_FS) { console.log("fs ftruncate " + openedFiles[fd].path); } + var file = openedFiles[fd]; if (size != file.buffer.contentSize) { file.buffer.setSize(size); file.dirty = true; - fileStats[file.path] = file.stat = { mtime: Date.now(), isDir: false, size: size }; + file.stat = { mtime: Date.now(), isDir: false, size: size }; } } function remove(path, cb) { path = normalizePath(path); + if (DEBUG_FS) { console.log("fs remove " + path); } if (openedFiles.findIndex(function(file) { return file && file.path === path; }) != -1) { - setZeroTimeout(function() { cb(false); }, 0); + setZeroTimeout(function() { cb(false); }); return; } @@ -280,13 +384,10 @@ var fs = (function() { } files.splice(index, 1); - asyncStorage.setItem(dir, files, function() { - asyncStorage.removeItem(path, function() { - removeStat(path, function() { - cb(true); - }); - }); - }); + store.setItem(dir, files); + store.removeItem(path); + removeStat(path); + cb(true); }); }); } @@ -302,43 +403,39 @@ var fs = (function() { } files.push(name); - asyncStorage.setItem(dir, files, function() { - asyncStorage.setItem(path, data, function() { - cb(true); - }); - }); + store.setItem(dir, files); + store.setItem(path, data); + cb(true); }); } function create(path, blob, cb) { path = normalizePath(path); + if (DEBUG_FS) { console.log("fs create " + path); } createInternal(path, blob, function(created) { if (created) { - setStat(path, { mtime: Date.now(), isDir: false, size: blob.size }, function() { - cb(created); - }); - } else { - cb(created); + setStat(path, { mtime: Date.now(), isDir: false, size: blob.size }); } + cb(created); }); } function mkdir(path, cb) { path = normalizePath(path); + if (DEBUG_FS) { console.log("fs mkdir " + path); } createInternal(path, [], function(created) { if (created) { - setStat(path, { mtime: Date.now(), isDir: true }, function() { - cb(created); - }); - } else { - cb(created); + setStat(path, { mtime: Date.now(), isDir: true }); } + cb(created); }); } function mkdirp(path, cb) { + if (DEBUG_FS) { console.log("fs mkdirp " + path); } + if (path[0] !== "/") { console.error("mkdirp called on relative path: " + path); cb(false); @@ -382,19 +479,12 @@ var fs = (function() { function size(path, cb) { path = normalizePath(path); + if (DEBUG_FS) { console.log("fs size " + path); } - if (fileStats[path] && typeof fileStats[path].size != "undefined") { - cb(fileStats[path].size); - return; - } - - asyncStorage.getItem(path, function(blob) { + store.getItem(path, function(blob) { if (blob == null || !(blob instanceof Blob)) { cb(-1); } else { - if (fileStats[path]) { - fileStats[path].size = blob.size; - } cb(blob.size); } }); @@ -405,9 +495,10 @@ var fs = (function() { function rename(oldPath, newPath, cb) { oldPath = normalizePath(oldPath); newPath = normalizePath(newPath); + if (DEBUG_FS) { console.log("fs rename " + oldPath + " -> " + newPath); } if (openedFiles.findIndex(function(file) { return file && file.path === oldPath; }) != -1) { - setZeroTimeout(function() { cb(false); }, 0); + setZeroTimeout(function() { cb(false); }); return; } @@ -417,7 +508,7 @@ var fs = (function() { return; } - asyncStorage.getItem(oldPath, function(data) { + store.getItem(oldPath, function(data) { if (data == null) { cb(false); return; @@ -439,37 +530,34 @@ var fs = (function() { }); } - function setStat(path, stat, cb) { - fileStats[path] = stat; - asyncStorage.setItem("!" + path, stat, cb); + function setStat(path, stat) { + if (DEBUG_FS) { console.log("fs setStat " + path); } + + store.setItem("!" + path, stat); } - function removeStat(path, cb) { - delete fileStats[path]; - asyncStorage.removeItem("!" + path, cb); + function removeStat(path) { + if (DEBUG_FS) { console.log("fs removeStat " + path); } + + store.removeItem("!" + path); } function stat(path, cb) { path = normalizePath(path); - - var stat = fileStats[path]; - if (stat) { - setZeroTimeout(function() { cb(stat); }, 0); - return; - } + if (DEBUG_FS) { console.log("fs stat " + path); } var file = openedFiles.find(function (file) { return file && file.stat && file.path === path }); if (file) { - setZeroTimeout(function() { cb(file.stat); }, 0); + setZeroTimeout(function() { cb(file.stat); }); return; } - asyncStorage.getItem("!" + path, function(stat) { - if (stat) { - fileStats[path] = stat; - } - cb(stat); - }); + store.getItem("!" + path, cb); + } + + function clear(cb) { + store.clear(); + initRootDir(cb || function() {}); } return { @@ -494,5 +582,6 @@ var fs = (function() { size: size, rename: rename, stat: stat, + clear: clear, }; })(); diff --git a/libs/pipe.js b/libs/pipe.js index 074b54642..fa0489d4b 100644 --- a/libs/pipe.js +++ b/libs/pipe.js @@ -78,46 +78,27 @@ var DumbPipe = { } }, - handleEvent: function(event) { - // To ensure we don't fill up the browser history over time, we navigate - // "back" every time the other side navigates us "forward" by changing - // the hash. This will trigger a second hashchange event; to avoid getting - // messages twice, we only get them for the second hashchange event, - // i.e. once we've returned to the hashless page, at which point a second - // call to window.history.back() will have had no effect. - // - // We only do this when we're in mozbrowser (i.e. window.parent === window), - // since window.history.back() affects the parent window otherwise. - // - if (window.parent === window) { - var hash = window.location.hash; - window.history.back(); - if (window.location.hash != hash) { - return; - } + receiveMessage: function(event) { + if (event.source === window) { + return; } - this.send({ command: "get" }, function(envelopes) { - envelopes.forEach((function(envelope) { - //console.log("inner recv: " + JSON.stringify(envelope)); - window.setZeroTimeout(function() { - if (this.recipients[envelope.pipeID]) { - try { - this.recipients[envelope.pipeID](envelope.message); - } catch(ex) { - console.error(ex + "\n" + ex.stack); - } - } else { - console.warn("nonexistent pipe " + envelope.pipeID + " received message " + - JSON.stringify(envelope.message)); - } - }.bind(this)); - }).bind(this)); - }.bind(this)); + var envelope = event.data; + + if (this.recipients[envelope.pipeID]) { + try { + this.recipients[envelope.pipeID](envelope.message); + } catch(ex) { + console.error(ex + "\n" + ex.stack); + } + } else { + console.warn("nonexistent pipe " + envelope.pipeID + " received message " + + JSON.stringify(envelope.message)); + } }, }; -window.addEventListener("hashchange", DumbPipe.handleEvent.bind(DumbPipe), false); +window.addEventListener("message", DumbPipe.receiveMessage.bind(DumbPipe), false); // If "mozbrowser" isn't enabled on the frame we're loaded in, then override // the alert/prompt functions to funnel messages to the endpoint in the parent. diff --git a/libs/urlparams.js b/libs/urlparams.js index 328e452fa..1396cf826 100644 --- a/libs/urlparams.js +++ b/libs/urlparams.js @@ -20,6 +20,7 @@ * midletClassName * network_mcc * network_mnc + * platform * profile * pushConn * pushMidlet @@ -38,7 +39,7 @@ var urlParams = (function() { params[param[0]] = param[1]; }); - params.args = (params.args || "").split(","); + params.args = (params.args || "").replace(".", "/", "g").split(","); return params; })(); diff --git a/main.html b/main.html index 275e7369c..8c1e088e4 100644 --- a/main.html +++ b/main.html @@ -28,7 +28,6 @@ - @@ -52,6 +51,7 @@ + diff --git a/main.js b/main.js index 0b5e179ab..beaf890cf 100644 --- a/main.js +++ b/main.js @@ -63,7 +63,7 @@ if (urlParams.pushConn && urlParams.pushMidlet) { var initFS = new Promise(function(resolve, reject) { fs.init(resolve); }).then(function() { - return Promise.all([ + var fsPromises = [ new Promise(function(resolve, reject) { fs.mkdir("/Persistent", resolve); }), @@ -81,7 +81,27 @@ var initFS = new Promise(function(resolve, reject) { } }); }), - ]); + ]; + + if (MIDP.midletClassName == "RunTests") { + fsPromises.push( + new Promise(function(resolve, reject) { + fs.exists("/_test.ks", function(exists) { + if (exists) { + resolve(); + } else { + load("certs/_test.ks", "blob").then(function(data) { + fs.create("/_test.ks", data, function() { + resolve(); + }); + }); + } + }); + }) + ); + } + + return Promise.all(fsPromises); }); // Mobile info gets accessed a lot, so we cache it on startup. @@ -118,22 +138,8 @@ if (urlParams.jad) { if (MIDP.midletClassName == "RunTests") { loadingPromises.push(loadScript("tests/native.js"), - loadScript("tests/override.js")); - loadingPromises.push( - new Promise(function(resolve, reject) { - fs.exists("/_test.ks", function(exists) { - if (exists) { - resolve(); - } else { - load("certs/_test.ks", "blob").then(function(data) { - fs.create("/_test.ks", data, function() { - resolve(); - }); - }); - } - }); - }) - ); + loadScript("tests/override.js"), + loadScript("tests/mozactivitymock.js")); } Promise.all(loadingPromises).then(function() { @@ -151,7 +157,7 @@ function toggle(button) { window.onload = function() { document.getElementById("clearstorage").onclick = function() { - asyncStorage.clear(); + fs.clear(); }; document.getElementById("trace").onclick = function() { VM.DEBUG = !VM.DEBUG; diff --git a/manifest.webapp b/manifest.webapp index 49b2a4206..f52e8f024 100644 --- a/manifest.webapp +++ b/manifest.webapp @@ -23,7 +23,10 @@ "mobilenetwork": { "description:": "Required to verify your phone number" }, - "browser": {} + "browser": {}, + "audio-capture": { + "description": "Required to capture audio via getUserMedia" + } }, "type": "privileged" } diff --git a/midp/device_control.js b/midp/device_control.js new file mode 100644 index 000000000..ae596a0f9 --- /dev/null +++ b/midp/device_control.js @@ -0,0 +1,28 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +'use strict'; + +Native.create("com/nokia/mid/ui/DeviceControl.startVibra.(IJ)V", function(freq, longDuration, _) { + // If method is called during a previously called vibration that has been + // activated from this method, the previous vibration is stopped and the new + // one is activated using the new set of parameters. + navigator.vibrate(0); + + // Value 0 can be used for detecting whether or not there is a vibration device. + if (freq === 0) { + return; + } + + var duration = longDuration.toNumber(); + + if (freq < 0 || freq > 100 || duration < 0) { + throw new JavaException("java/lang/IllegalArgumentException"); + } + + navigator.vibrate(duration); +}); + +Native.create("com/nokia/mid/ui/DeviceControl.stopVibra.()V", function() { + navigator.vibrate(0); +}); diff --git a/midp/fs.js b/midp/fs.js index 8f39a1bc5..41f511fc2 100644 --- a/midp/fs.js +++ b/midp/fs.js @@ -109,25 +109,17 @@ Native.create("com/sun/midp/rms/RecordStoreFile.writeBytes.(I[BII)V", function(h }); Native.create("com/sun/midp/rms/RecordStoreFile.commitWrite.(I)V", function(handle) { - return new Promise(function(resolve, reject) { - fs.flush(handle, resolve); - }); -}, true); + fs.flush(handle); +}); Native.create("com/sun/midp/rms/RecordStoreFile.closeFile.(I)V", function(handle) { - return new Promise(function(resolve, reject) { - fs.close(handle, resolve); - }); -}, true); + fs.close(handle); +}); Native.create("com/sun/midp/rms/RecordStoreFile.truncateFile.(II)V", function(handle, size) { - return new Promise(function(resolve, reject) { - fs.flush(handle, function() { - fs.ftruncate(handle, size); - resolve(); - }); - }); -}, true); + fs.flush(handle); + fs.ftruncate(handle, size); +}); MIDP.RecordStoreCache = []; @@ -442,7 +434,8 @@ Native.create("com/ibm/oti/connection/file/Connection.truncateImpl.([BJ)V", func } fs.ftruncate(fd, newLength.toNumber()); - fs.close(fd, resolve); + fs.close(fd); + resolve(); }); }); }, true); @@ -500,10 +493,8 @@ Native.create("com/ibm/oti/connection/file/FCInputStream.closeImpl.(I)V", functi }); Native.create("com/ibm/oti/connection/file/FCOutputStream.closeImpl.(I)V", function(fd) { - return new Promise(function(resolve, reject) { - fs.close(fd, resolve); - }); -}, true); + fs.close(fd); +}); Native.create("com/ibm/oti/connection/file/FCOutputStream.openImpl.([B)I", function(jPath) { var path = util.decodeUtf8(jPath); @@ -559,10 +550,8 @@ Native.create("com/ibm/oti/connection/file/FCOutputStream.openOffsetImpl.([BJ)I" }, true); Native.create("com/ibm/oti/connection/file/FCOutputStream.syncImpl.(I)V", function(fd) { - return new Promise(function(resolve, reject) { - fs.flush(fd, resolve); - }); -}, true); + fs.flush(fd); +}); Native.create("com/ibm/oti/connection/file/FCOutputStream.writeByteImpl.(II)V", function(val, fd) { var buf = new Uint8Array(1); @@ -630,10 +619,8 @@ function(handle, buffer, offset, length) { }); Native.create("com/sun/midp/io/j2me/storage/RandomAccessStream.commitWrite.(I)V", function(handle) { - return new Promise(function(resolve, reject) { - fs.flush(handle, resolve); - }); -}, true); + fs.flush(handle); +}); Native.create("com/sun/midp/io/j2me/storage/RandomAccessStream.position.(II)V", function(handle, position) { fs.setpos(handle, position); @@ -650,7 +637,11 @@ Native.create("com/sun/midp/io/j2me/storage/RandomAccessStream.sizeOf.(I)I", fun }); Native.create("com/sun/midp/io/j2me/storage/RandomAccessStream.close.(I)V", function(handle) { - return new Promise(function(resolve, reject) { - fs.close(handle, resolve); - }); -}, true); + fs.close(handle); +}); + +Native.create("javax/microedition/io/file/FileSystemRegistry.getRootsImpl.()[Ljava/lang/String;", function() { + var array = util.newArray("[Ljava/lang/String;", 1); + array[0] = util.newString(""); + return array; +}); diff --git a/midp/gfx.js b/midp/gfx.js index 412fbd621..744426ca5 100644 --- a/midp/gfx.js +++ b/midp/gfx.js @@ -816,10 +816,9 @@ texture = texture.canvas; // Render the canvas, not the context. } - var w = sw, h = sh; var g = this; withGraphics(g, function(c) { - withAnchor(g, c, anchor, x, y, w, h, function(x, y) { + withAnchor(g, c, anchor, x, y, sw, sh, function(x, y) { c.translate(x, y); if (transform === TRANS_MIRROR || transform === TRANS_MIRROR_ROT180) c.scale(-1, 1); @@ -831,7 +830,7 @@ c.rotate(Math.PI); if (transform === TRANS_ROT270 || transform === TRANS_MIRROR_ROT270) c.rotate(1.5 * Math.PI); - c.drawImage(texture, sx, sy, w, h, 0, 0, sw, sh); + c.drawImage(texture, sx, sy, sw, sh, 0, 0, sw, sh); }); }); }); @@ -843,12 +842,11 @@ withClip(g, c, x1, y1, function(x, y) { withSize(dx, dy, function(dx, dy) { withPixel(g, c, function() { - var ctx = MIDP.Context2D; - ctx.beginPath(); - ctx.moveTo(x, y); - ctx.lineTo(x + dx, y + dy); - ctx.stroke(); - ctx.closePath(); + c.beginPath(); + c.moveTo(x, y); + c.lineTo(x + dx, y + dy); + c.stroke(); + c.closePath(); }); }); }); @@ -1094,4 +1092,84 @@ return dirtyEditors.shift(); }); + + var initialWindowHeight = window.innerHeight; + var isVKVisible = false; + var keyboardHeight = 0; + var pendingShowNotify = false; + var pendingHideNotify = false; + var keyboardVisibilityListenerResolve; + window.addEventListener("resize", function(evt) { + if (window.innerHeight < initialWindowHeight) { + if (isVKVisible) { + console.warn("Window shrunk but we thought the keyboard was already visible!"); + } + isVKVisible = true; + keyboardHeight = initialWindowHeight - window.innerHeight; + if (pendingHideNotify) { + pendingHideNotify = false; + return; + } else if (keyboardVisibilityListenerResolve) { + keyboardVisibilityListenerResolve(true); + keyboardVisibilityListenerResolve = null; + } else { + pendingShowNotify = true; + } + } else if (window.innerHeight >= initialWindowHeight) { + if (window.innerHeight > initialWindowHeight) { + console.warn("Window grew beyond initial size!"); + initialWindowHeight = window.innerHeight; + } + if (!isVKVisible) { + console.warn("Window grew but we thought the keyboard was already hidden!"); + } + isVKVisible = false; + keyboardHeight = 0; + if (pendingShowNotify) { + pendingShowNotify = false; + return; + } else if (keyboardVisibilityListenerResolve) { + keyboardVisibilityListenerResolve(false); + keyboardVisibilityListenerResolve = null; + } else { + pendingHideNotify = true; + } + } + }); + + Native.create("com/nokia/mid/ui/VirtualKeyboard.isVisible.()Z", function() { + return isVKVisible; + }); + + Native.create("com/nokia/mid/ui/VirtualKeyboard.getXPosition.()I", function() { + return 0; + }); + + Native.create("com/nokia/mid/ui/VirtualKeyboard.getYPosition.()I", function() { + // We should return the number of pixels between the top of the + // screen and the top of the keyboard + return window.innerHeight; + }); + + Native.create("com/nokia/mid/ui/VirtualKeyboard.getWidth.()I", function() { + return window.innerWidth; + }); + + Native.create("com/nokia/mid/ui/VirtualKeyboard.getHeight.()I", function() { + return keyboardHeight; + }); + + Native.create("com/nokia/mid/ui/VKVisibilityNotificationRunnable.sleepUntilVKVisibilityChange.()Z", function() { + return new Promise(function(resolve, reject) { + if (pendingShowNotify) { + resolve(true); + pendingShowNotify = false; + } else if (pendingHideNotify) { + resolve(false); + pendingHideNotify = false; + } else { + keyboardVisibilityListenerResolve = resolve; + } + }); + }, true); })(Native); diff --git a/midp/localmsg.js b/midp/localmsg.js index 0ff29eb4a..673b8a7cc 100644 --- a/midp/localmsg.js +++ b/midp/localmsg.js @@ -187,6 +187,56 @@ NokiaMessagingLocalMsgConnection.prototype.sendMessageToServer = function(messag }); } +var NokiaSASrvRegLocalMsgConnection = function() { + LocalMsgConnection.call(this); +}; + +NokiaSASrvRegLocalMsgConnection.prototype = Object.create(LocalMsgConnection.prototype); + +NokiaSASrvRegLocalMsgConnection.prototype.sendMessageToServer = function(message) { + var decoder = new DataDecoder(message.data, message.offset, message.length); + + decoder.getStart(DataType.STRUCT); + var name = decoder.getValue(DataType.METHOD); + + var encoder = new DataEncoder(); + + switch (name) { + case "Common": + encoder.putStart(DataType.STRUCT, "event"); + encoder.put(DataType.METHOD, "name", "Common"); + encoder.putStart(DataType.STRUCT, "message"); + encoder.put(DataType.METHOD, "name", "ProtocolVersion"); + encoder.put(DataType.STRING, "version", "2.0"); + encoder.putEnd(DataType.STRUCT, "message"); + encoder.putEnd(DataType.STRUCT, "event"); + break; + case "Discovery": + encoder.putStart(DataType.STRUCT, "event"); + encoder.put(DataType.METHOD, "name", "Discovery"); + encoder.put(DataType.BYTE, "unknown_byte_1", 1); + encoder.put(DataType.STRING, "unknown_string_1", ""); + encoder.putStart(DataType.ARRAY, "services"); + encoder.putStart(DataType.STRUCT, "service"); + encoder.put(DataType.STRING, "ServiceName", "file_ui"); + encoder.put(DataType.URI, "ServiceURI", "nokia.file-ui"); + encoder.put(DataType.STRING, "unknown_string_2", ""); + encoder.put(DataType.WSTRING, "unknown_string_3", ""); + encoder.put(DataType.STRING, "unknown_string_4", ""); + encoder.putEnd(DataType.STRUCT, "service"); + encoder.putEnd(DataType.ARRAY, "services"); + encoder.putEnd(DataType.STRUCT, "event"); + break; + } + + var data = new TextEncoder().encode(encoder.getData()); + this.sendMessageToClient({ + data: data, + length: data.length, + offset: 0, + }); +}; + var NokiaPhoneStatusLocalMsgConnection = function() { LocalMsgConnection.call(this); }; @@ -289,6 +339,10 @@ NokiaContactsLocalMsgConnection.prototype.encodeContact = function(encoder, cont } NokiaContactsLocalMsgConnection.prototype.sendContact = function(trans_id, contact) { + if (!contact.tel) { + return; + } + var encoder = new DataEncoder(); encoder.putStart(DataType.STRUCT, "event"); encoder.put(DataType.METHOD, "name", "Notify"); @@ -344,7 +398,12 @@ NokiaContactsLocalMsgConnection.prototype.sendMessageToServer = function(message util.decodeUtf8(new Uint8Array(message.data.buffer, message.offset, message.length))); } - contacts.getNext((function(contact) { + var gotContact = (function(contact) { + if (contact && !contact.tel) { + contacts.getNext(gotContact); + return; + } + var encoder = new DataEncoder(); encoder.putStart(DataType.STRUCT, "event"); encoder.put(DataType.METHOD, "name", "getFirst"); @@ -365,7 +424,9 @@ NokiaContactsLocalMsgConnection.prototype.sendMessageToServer = function(message length: data.length, offset: 0, }); - }).bind(this)); + }).bind(this); + + contacts.getNext(gotContact); break; case "getNext": @@ -484,16 +545,38 @@ NokiaFileUILocalMsgConnection.prototype.sendMessageToServer = function(message) var multipleSelection = decoder.getValue(DataType.BOOLEAN); var startingURL = decoder.getValue(DataType.STRING); + var accept = ''; + + switch (mediaType) { + case "Picture": + accept = "image/*"; + break; + + case "Video": + accept = "video/*"; + break; + + case "Music": + accept = "audio/*"; + break; + + default: + throw new Error("Media type '" + mediaType + "' not supported"); + } + var el = document.getElementById('nokia-fileui-prompt').cloneNode(true); el.style.display = 'block'; el.classList.add('visible'); + var fileInput = el.querySelector('input'); + fileInput.accept = accept; + var btnDone = el.querySelector('button.recommend'); btnDone.disabled = true; var selectedFile = null; - el.querySelector('input').addEventListener('change', function() { + fileInput.addEventListener('change', function() { btnDone.disabled = false; selectedFile = this.files[0]; }); @@ -663,6 +746,7 @@ MIDP.LocalMsgConnections["nokia.messaging"] = new NokiaMessagingLocalMsgConnecti MIDP.LocalMsgConnections["nokia.phone-status"] = new NokiaPhoneStatusLocalMsgConnection(); MIDP.LocalMsgConnections["nokia.file-ui"] = new NokiaFileUILocalMsgConnection(); MIDP.LocalMsgConnections["nokia.image-processing"] = new NokiaImageProcessingLocalMsgConnection(); +MIDP.LocalMsgConnections["nokia.sa.service-registry"] = new NokiaSASrvRegLocalMsgConnection(); Native.create("org/mozilla/io/LocalMsgConnection.init.(Ljava/lang/String;)V", function(jName) { var name = util.fromJavaString(jName); diff --git a/midp/media.js b/midp/media.js index f8a021415..d74386d12 100644 --- a/midp/media.js +++ b/midp/media.js @@ -3,23 +3,32 @@ 'use strict'; -var ContentTypes = { +var Media = {}; + +Media.ContentTypes = { memory: [ ], file: [ + "audio/ogg", "audio/x-wav", "audio/mpeg", + "image/jpeg", + "image/png", ], http: [ "audio/x-wav", "audio/mpeg", + "image/jpeg", + "image/png", ], https: [ "audio/x-wav", "audio/mpeg", + "image/jpeg", + "image/png", ], rtp: [], @@ -29,8 +38,7 @@ var ContentTypes = { capture: [] }; -var ListCache = { - +Media.ListCache = { create: function(data) { var id = this._nextId; this._cached[id] = data; @@ -53,36 +61,53 @@ var ListCache = { _nextId: 1 } -var extToFormat = new Map([ +Media.extToFormat = new Map([ ["mp3", "MPEG_layer_3"], + ["jpg", "JPEG"], + ["jpeg", "JPEG"], + ["png", "PNG"], +]); + +Media.contentTypeToFormat = new Map([ + ["audio/ogg", "ogg"], + ["audio/amr", "amr"], + ["audio/x-wav", "wav"], + ["audio/mpeg", "MPEG_layer_3"], + ["image/jpeg", "JPEG"], + ["image/png", "PNG"], ]); +Media.supportedAudioFormats = ["MPEG_layer_3", "wav", "amr", "ogg"]; +Media.supportedImageFormats = ["JPEG", "PNG"]; + +Media.EVENT_MEDIA_SNAPSHOT_FINISHED = 11; + Native.create("com/sun/mmedia/DefaultConfiguration.nListContentTypesOpen.(Ljava/lang/String;)I", function(jProtocol) { var protocol = util.fromJavaString(jProtocol); var types = []; if (protocol) { - types = ContentTypes[protocol]; + types = Media.ContentTypes[protocol]; if (!types) { console.warn("Unknown protocol type: " + protocol); return 0; } } else { - for (var p in ContentTypes) { - ContentTypes[p].forEach(function(type) { + for (var p in Media.ContentTypes) { + Media.ContentTypes[p].forEach(function(type) { if (types.indexOf(type) === -1) { types.push(type); } - }) + }); } } if (types.length == 0) { return 0; } - return ListCache.create(types); + return Media.ListCache.create(types); }); Native.create("com/sun/mmedia/DefaultConfiguration.nListContentTypesNext.(I)Ljava/lang/String;", function(hdlr) { - var cached = ListCache.get(hdlr); + var cached = Media.ListCache.get(hdlr); if (!cached) { console.error("Invalid hdlr: " + hdlr); return null; @@ -91,25 +116,25 @@ Native.create("com/sun/mmedia/DefaultConfiguration.nListContentTypesNext.(I)Ljav }); Native.create("com/sun/mmedia/DefaultConfiguration.nListContentTypesClose.(I)V", function(hdlr) { - ListCache.remove(hdlr); + Media.ListCache.remove(hdlr); }); Native.create("com/sun/mmedia/DefaultConfiguration.nListProtocolsOpen.(Ljava/lang/String;)I", function(jMime) { var mime = util.fromJavaString(jMime); var protocols = []; - for (var protocol in ContentTypes) { - if (!mime || ContentTypes[protocol].indexOf(mime) >= 0) { + for (var protocol in Media.ContentTypes) { + if (!mime || Media.ContentTypes[protocol].indexOf(mime) >= 0) { protocols.push(protocol); } } if (!protocols.length) { return 0; } - return ListCache.create(protocols); + return Media.ListCache.create(protocols); }); Native.create("com/sun/mmedia/DefaultConfiguration.nListProtocolsNext.(I)Ljava/lang/String;", function(hdlr) { - var cached = ListCache.get(hdlr); + var cached = Media.ListCache.get(hdlr); if (!cached) { console.error("Invalid hdlr: " + hdlr); return null; @@ -118,116 +143,467 @@ Native.create("com/sun/mmedia/DefaultConfiguration.nListProtocolsNext.(I)Ljava/l }); Native.create("com/sun/mmedia/DefaultConfiguration.nListProtocolsClose.(I)V", function(hdlr) { - ListCache.remove(hdlr); + Media.ListCache.remove(hdlr); }); -var PlayerCache = { +Media.PlayerCache = { }; -function Player(url) { - this.url = url; - // this.mediaFormat will only be updated by PlayerImpl.nGetMediaFormat. - this.mediaFormat = url ? this.guessFormatFromURL(url) : "UNKNOWN"; - this.contentType = ""; - this.wholeContentSize = -1; - this.contentSize = 0; - this.volume = -1; - this.isMuted = false; +function AudioPlayer(playerContainer) { + this.playerContainer = playerContainer; - /* @type {Int8Array} */ - this.data = null; + this.isMuted = false; /* @type {AudioBuffer} */ this.audioBuffer = null; - /* @type {AudioContext} */ - this.audioContext = null; + this.audioContext = new AudioContext(); /* @type {AudioBufferSourceNode} */ this.source = null; /* * Audio gain node used to control volume. - * @type {GainNode} */ - this.gainNode = null; + this.gainNode = this.audioContext.createGain(); + + this.volume = Math.round(this.gainNode.gain.value * 100); + + this.gainNode.connect(this.audioContext.destination); this.isPlaying = false; this.startTime = 0; this.stopTime = 0; this.duration = 0; + + this.isVideoControlSupported = false; + this.isVolumeControlSupported = true; } -// default buffer size 1 MB -Player.DEFAULT_BUFFER_SIZE = 1024 * 1024; +AudioPlayer.prototype.realize = function() { + return new Promise(function(resolve, reject) { resolve(true); }); +} -Player.prototype.guessFormatFromURL = function() { - return extToFormat.get(this.url.substr(this.url.lastIndexOf(".") + 1)) || "UNKNOWN"; +AudioPlayer.prototype.play = function() { + var offset = this.stopTime - this.startTime; + this.source = this.audioContext.createBufferSource(); + this.source.buffer = this.cloneBuffer(); + this.source.connect(this.gainNode || this.audioContext.destination); + this.source.start(0, offset); + this.isPlaying = true; + this.startTime = this.audioContext.currentTime - offset; + this.source.onended = function() { + this.close(); + }.bind(this); } -Player.prototype.realize = function(contentType) { - if (contentType) { - switch (contentType) { - case "audio/x-wav": - case "audio/amr": - case "audio/mpeg": - this.contentType = contentType; - break; - default: - console.warn("Unsupported content type: " + contentType); - return false; - } +AudioPlayer.prototype.start = function() { + if (this.playerContainer.contentSize > 0) { + this.decode(this.playerContainer.data.subarray(0, this.playerContainer.contentSize), function(decoded) { + // Save a copy of the audio buffer for resuming or replaying. + this.audioBuffer = decoded; + this.duration = decoded.duration; + this.play(); + }.bind(this)); + + return; } - this.audioContext = new AudioContext(); - if (this.isVolumeControlSupported()) { - this.gainNode = this.audioContext.createGain(); - this.volume = Math.round(this.gainNode.gain.value * 100); - this.gainNode.connect(this.audioContext.destination); + + console.warn("Cannot start playing."); +} + +AudioPlayer.prototype.pause = function() { + if (!this.isPlaying) { + return; } - return true; -}; -Player.prototype.close = function() { + this.isPlaying = false; + this.source.onended = null; + this.stopTime = this.audioContext.currentTime; + this.source.stop(); + this.source.disconnect(); + this.source = null; +} + +AudioPlayer.prototype.resume = function() { + if (this.isPlaying) { + return; + } + + if (this.stopTime - this.startTime >= this.duration) { + return; + } + + this.play(); +} + +AudioPlayer.prototype.close = function() { if (this.source) { this.source.stop(); this.source.disconnect(); this.source = null; } + if (this.gainNode) { this.gainNode.disconnect(); this.gainNode = null; } + this.audioBuffer = null; - this.data = null; this.startTime = 0; this.stopTime = 0; this.isPlaying = false; -}; +} -/** - * @return current time in ms. - */ -Player.prototype.getMediaTime = function() { +AudioPlayer.prototype.getMediaTime = function() { if (!this.audioContext) { return -1; } + var time = 0; + if (this.isPlaying) { time = this.audioContext.currentTime - this.startTime; } else { time = Math.min(this.duration, this.stopTime - this.startTime); } + return Math.round(time * 1000); +} + +AudioPlayer.prototype.cloneBuffer = function() { + var buffer = this.audioBuffer; + var cloned = this.audioContext.createBuffer( + buffer.numberOfChannels, + buffer.length, + buffer.sampleRate + ); + + for (var i = 0; i < buffer.numberOfChannels; ++i) { + var channel = buffer.getChannelData(i); + cloned.getChannelData(i).set(new Float32Array(channel)); + } + return cloned; +}; + +AudioPlayer.prototype.decode = function(encoded, callback) { + this.audioContext.decodeAudioData(encoded.buffer, callback); +}; + +AudioPlayer.prototype.getVolume = function() { + return this.volume; +}; + +AudioPlayer.prototype.setVolume = function(level) { + if (!this.gainNode) { + return -1; + } + if (level < 0) { + level = 0; + } else if (level > 100) { + level = 100; + } + this.volume = level; + if (!this.isMuted) { + this.gainNode.gain.value = level / 100; + } + return level; +} + +AudioPlayer.prototype.getMute = function() { + return this.isMuted; +} + +AudioPlayer.prototype.setMute = function(mute) { + if (this.isMuted === mute) { + return; + } + this.isMuted = mute; + if (!this.gainNode) { + return; + } + if (mute) { + this.gainNode.gain.value = 0; + } else { + this.gainNode.gain.value = this.volume / 100; + } +} + +AudioPlayer.prototype.getDuration = function() { + if (!this.audioBuffer) { + return -1; // Player.TIME_UNKNOWN + } + + return this.duration * 1000; +} + +function ImagePlayer(playerContainer) { + this.url = playerContainer.url; + + this.image = new Image(); + this.image.style.position = "absolute"; + this.image.style.visibility = "hidden"; + + this.isVideoControlSupported = true; + this.isAudioControlSupported = false; +} + +ImagePlayer.prototype.realize = function() { + return new Promise((function(resolve, reject) { + if (this.url.startsWith("file")) { + fs.open(this.url.substring(7), (function(fd) { + var imgData = fs.read(fd); + fs.close(fd); + + this.image.src = URL.createObjectURL(new Blob([ imgData ])); + resolve(true); + }).bind(this)); + } else { + this.image.src = this.url; + resolve(true); + } + }).bind(this)); +} + +ImagePlayer.prototype.start = function() { +} + +ImagePlayer.prototype.pause = function() { +} + +ImagePlayer.prototype.close = function() { + if (this.image.parentNode) { + document.getElementById("display").removeChild(this.image); + } +} + +ImagePlayer.prototype.getMediaTime = function() { + return -1; +} + +ImagePlayer.prototype.getWidth = function() { + return this.image.naturalWidth; +} + +ImagePlayer.prototype.getHeight = function() { + return this.image.naturalHeight; +} + +ImagePlayer.prototype.setLocation = function(x, y, w, h) { + this.image.style.left = x + "px"; + this.image.style.top = y + "px"; + this.image.style.width = w + "px"; + this.image.style.height = h + "px"; + document.getElementById("display").appendChild(this.image); +} + +ImagePlayer.prototype.setVisible = function(visible) { + this.image.style.visibility = visible ? "visible" : "hidden"; +} + +function ImageRecorder(playerContainer) { + this.playerContainer = playerContainer; + + this.sender = null; + + this.width = -1; + this.height = -1; + + this.isVideoControlSupported = true; + this.isAudioControlSupported = false; + + this.realizeResolver = null; + + this.snapshotData = null; +} + +ImageRecorder.prototype.realize = function() { + return new Promise((function(resolve, reject) { + this.realizeResolver = resolve; + this.sender = DumbPipe.open("camera", {}, this.recipient.bind(this)); + }).bind(this)); +} + +ImageRecorder.prototype.recipient = function(message) { + switch (message.type) { + case "gotstream": + this.width = message.width; + this.height = message.height; + this.realizeResolver(true); + this.realizeResolver = null; + break; + + case "snapshot": + this.snapshotData = new Int8Array(message.data); + + MIDP.sendNativeEvent({ + type: MIDP.MMAPI_EVENT, + intParam1: this.playerContainer.handle, + intParam2: 0, + intParam3: 0, + intParam4: Media.EVENT_MEDIA_SNAPSHOT_FINISHED, + }, MIDP.foregroundIsolateId); + + break; + } +} + +ImageRecorder.prototype.start = function() { +} + +ImageRecorder.prototype.pause = function() { +} + +ImageRecorder.prototype.close = function() { + this.sender({ type: "close" }); +} + +ImageRecorder.prototype.getMediaTime = function() { + return -1; +} + +ImageRecorder.prototype.getWidth = function() { + return this.width; +} + +ImageRecorder.prototype.getHeight = function() { + return this.height; +} + +ImageRecorder.prototype.setLocation = function(x, y, w, h) { + var displayElem = document.getElementById("display"); + this.sender({ + type: "setPosition", + x: x + displayElem.offsetLeft, + y: y + displayElem.offsetTop, + w: w, + h: h, + }); +} + +ImageRecorder.prototype.setVisible = function(visible) { + this.sender({ type: "setVisible", visible: visible }); +} + +ImageRecorder.prototype.startSnapshot = function(imageType) { + var type = imageType ? this.playerContainer.getEncodingParam(imageType) : "image/jpeg"; + if (type === "jpeg") { + type = "image/jpeg"; + } + + this.sender({ type: "snapshot", imageType: type }); +} + +ImageRecorder.prototype.getSnapshotData = function(imageType) { + return this.snapshotData; +} + +function PlayerContainer(url) { + this.url = url; + + this.mediaFormat = url ? this.guessFormatFromURL(url) : "UNKNOWN"; + this.contentType = ""; + + this.wholeContentSize = -1; + this.contentSize = 0; + this.data = null; + + this.player = null; +} + +// default buffer size 1 MB +PlayerContainer.DEFAULT_BUFFER_SIZE = 1024 * 1024; + +PlayerContainer.prototype.isImageCapture = function() { + return !!(this.url && this.url.startsWith("capture://image")); +}; + +PlayerContainer.prototype.isAudioCapture = function() { + return !!(this.url && this.url.startsWith("capture://audio")); +}; + +PlayerContainer.prototype.getEncodingParam = function(url) { + var encoding = null; + + var idx = url.indexOf("encoding="); + if (idx > -1) { + var encodingKeyPair = url.substring(idx).split("&")[0].split("="); + encoding = encodingKeyPair.length == 2 ? encodingKeyPair[1] : encoding; + } + + return encoding; +}; + +PlayerContainer.prototype.guessFormatFromURL = function() { + if (this.isAudioCapture()) { + var encoding = "audio/ogg" || this.getEncodingParam(this.url); // Same as system property |audio.encodings| + + var format = Media.contentTypeToFormat.get(encoding); + + return format || "UNKNOWN"; + } + + if (this.isImageCapture()) { + return "JPEG"; + } + + return Media.extToFormat.get(this.url.substr(this.url.lastIndexOf(".") + 1)) || "UNKNOWN"; +} + +PlayerContainer.prototype.realize = function(contentType) { + return new Promise((function(resolve, reject) { + if (contentType) { + this.contentType = contentType; + this.mediaFormat = Media.contentTypeToFormat.get(contentType) || this.mediaFormat; + if (this.mediaFormat === "UNKNOWN") { + console.warn("Unsupported content type: " + contentType); + resolve(false); + return; + } + } + + if (Media.supportedAudioFormats.indexOf(this.mediaFormat) !== -1) { + this.player = new AudioPlayer(this); + if (this.isAudioCapture()) { + this.audioRecorder = new AudioRecorder(); + } + this.player.realize().then(resolve); + } else if (Media.supportedImageFormats.indexOf(this.mediaFormat) !== -1) { + if (this.isImageCapture()) { + this.player = new ImageRecorder(this); + } else { + this.player = new ImagePlayer(this); + } + this.player.realize().then(resolve); + } else { + console.warn("Unsupported media format (" + this.mediaFormat + ") for " + this.url); + resolve(false); + } + }).bind(this)); +}; + +PlayerContainer.prototype.close = function() { + this.data = null; + if (this.player) { + this.player.close(); + } }; -Player.prototype.getBufferSize = function() { - return this.wholeContentSize === -1 ? Player.DEFAULT_BUFFER_SIZE : +/** + * @return current time in ms. + */ +PlayerContainer.prototype.getMediaTime = function() { + return this.player.getMediaTime(); +}; + +PlayerContainer.prototype.getBufferSize = function() { + return this.wholeContentSize === -1 ? PlayerContainer.DEFAULT_BUFFER_SIZE : this.wholeContentSize; }; -Player.prototype.getMediaFormat = function() { - if (this.url === null || this.contentSize === 0) { +PlayerContainer.prototype.getMediaFormat = function() { + if (this.contentSize === 0) { return this.mediaFormat; } @@ -248,34 +624,32 @@ Player.prototype.getMediaFormat = function() { return "mid"; } + // https://wiki.xiph.org/Ogg#Detecting_Ogg_files_and_extracting_information + if (headerString.indexOf("OggS") === 0) { + return "ogg"; + } + return this.mediaFormat; }; -Player.prototype.isVolumeControlSupported = function() { - if (this.mediaFormat !== "UNKNOWN") { - switch (this.mediaFormat) { - case "amr": - case "wav": - case "MPEG_layer_3": - return true; - default: - return false; - } - } - if (this.contentType) { - switch (this.contentType) { - case "audio/amr": - case "audio/x-wav": - case "audio/mpeg": - return true; - default: - return false; - } - } - return false; +PlayerContainer.prototype.getContentType = function() { + return this.contentType; +}; + +PlayerContainer.prototype.isHandledByDevice = function() { + // TODO: Handle download in JS also for audio formats + return this.url !== null && Media.supportedAudioFormats.indexOf(this.mediaFormat) === -1; +}; + +PlayerContainer.prototype.isVideoControlSupported = function() { + return this.player.isVideoControlSupported; +}; + +PlayerContainer.prototype.isVolumeControlSupported = function() { + return this.player.isVolumeControlSupported; }; -Player.prototype.writeBuffer = function(buffer) { +PlayerContainer.prototype.writeBuffer = function(buffer) { if (this.contentSize === 0) { this.data = util.newPrimitiveArray("B", this.getBufferSize()); } @@ -284,120 +658,219 @@ Player.prototype.writeBuffer = function(buffer) { this.contentSize += buffer.length; }; -Player.prototype.play = function() { - var offset = this.stopTime - this.startTime; - this.source = this.audioContext.createBufferSource(); - this.source.buffer = this.cloneBuffer(); - this.source.connect(this.gainNode || this.audioContext.destination); - this.source.start(0, offset); - this.isPlaying = true; - this.startTime = this.audioContext.currentTime - offset; - this.source.onended = function() { - this.close(); - }.bind(this); +PlayerContainer.prototype.play = function() { + this.player.play(); }; -Player.prototype.start = function() { - return new Promise(function(resolve, reject) { - if (this.contentSize > 0) { - this.decode(this.data.subarray(0, this.contentSize), function(decoded) { - // Save a copy of the audio buffer for resumimg or replaying. - this.audioBuffer = decoded; - this.duration = decoded.duration; - this.play(); - resolve(); - }.bind(this)); - return; - } - console.warn("Cannot start playing."); - resolve(); - }.bind(this)); +PlayerContainer.prototype.start = function() { + this.player.start(); }; -Player.prototype.pause = function() { - if (!this.isPlaying) { - return; - } - this.isPlaying = false; - this.source.onended = null; - this.stopTime = this.audioContext.currentTime; - this.source.stop(); - this.source.disconnect(); - this.source = null; +PlayerContainer.prototype.pause = function() { + this.player.pause(); }; -Player.prototype.resume = function() { - if (this.isPlaying) { - return; - } - if (this.stopTime - this.startTime >= this.duration) { - return; - } - this.play(); +PlayerContainer.prototype.resume = function() { + this.player.resume(); }; -Player.prototype.cloneBuffer = function() { - var buffer = this.audioBuffer; - var cloned = this.audioContext.createBuffer( - buffer.numberOfChannels, - buffer.length, - buffer.sampleRate - ); +PlayerContainer.prototype.getVolume = function() { + return this.player.getVolume(); +}; - for (var i = 0; i < buffer.numberOfChannels; ++i) { - var channel = buffer.getChannelData(i); - cloned.getChannelData(i).set(new Float32Array(channel)); - } - return cloned; +PlayerContainer.prototype.setVolume = function(level) { + this.player.setVolume(level); }; -Player.prototype.decode = function(encoded, callback) { - this.audioContext.decodeAudioData(encoded.buffer, callback); +PlayerContainer.prototype.getMute = function() { + return this.player.getMute(); }; -Player.prototype.getVolume = function() { - return this.volume; +PlayerContainer.prototype.setMute = function(mute) { + return this.player.setMute(mute); }; -Player.prototype.setVolume = function(level) { - if (!this.gainNode) { - return -1; - } - if (level < 0) { - level = 0; - } else if (level > 100) { - level = 100; - } - this.volume = level; - if (!this.isMuted) { - this.gainNode.gain.value = level / 100; - } - return level; +PlayerContainer.prototype.getWidth = function() { + return this.player.getWidth(); +} + +PlayerContainer.prototype.getHeight = function() { + return this.player.getHeight(); +} + +PlayerContainer.prototype.setLocation = function(x, y, w, h) { + this.player.setLocation(x, y, w, h); +} + +PlayerContainer.prototype.setVisible = function(visible) { + this.player.setVisible(visible); +} + +PlayerContainer.prototype.getRecordedSize = function() { + return this.audioRecorder.data.byteLength; }; -Player.prototype.getMute = function() { - return this.isMuted; +PlayerContainer.prototype.getRecordedData = function(offset, size, buffer) { + var toRead = (size < this.audioRecorder.data.length) ? size : this.audioRecorder.data.byteLength; + buffer.set(this.audioRecorder.data.subarray(0, toRead), offset); + this.audioRecorder.data = new Uint8Array(this.audioRecorder.data.buffer.slice(toRead)); }; -Player.prototype.setMute = function(mute) { - if (this.isMuted === mute) { +PlayerContainer.prototype.startSnapshot = function(imageType) { + this.player.startSnapshot(imageType); +} + +PlayerContainer.prototype.getSnapshotData = function() { + return this.player.getSnapshotData(); +} + +PlayerContainer.prototype.getDuration = function() { + return this.player.getDuration(); +} + +var AudioRecorder = function(aMimeType) { + this.mimeType = aMimeType || "audio/ogg"; + this.eventListeners = {}; + this.data = new Uint8Array(); + this.sender = DumbPipe.open("audiorecorder", { + mimeType: this.mimeType + }, this.recipient.bind(this)); +}; + +AudioRecorder.prototype.recipient = function(message) { + var callback = this["on" + message.type]; + if (typeof callback === "function") { + callback(message); + } + + if (this.eventListeners[message.type]) { + this.eventListeners[message.type].forEach(function(listener) { + if (typeof listener === "function") { + listener(message); + } + }); + } +}; + +AudioRecorder.prototype.addEventListener = function(name, callback) { + if (!callback || !name) { return; } - this.isMuted = mute; - if (!this.gainNode) { + + if (!this.eventListeners[name]) { + this.eventListeners[name] = []; + } + + this.eventListeners[name].push(callback); +}; + +AudioRecorder.prototype.removeEventListener = function(name, callback) { + if (!name || !callback || !this.eventListeners[name]) { return; } - if (mute) { - this.gainNode.gain.value = 0; - } else { - this.gainNode.gain.value = this.volume / 100; + + var newArray = []; + this.eventListeners[name].forEach(function(listener) { + if (callback != listener) { + newArray.push(listener); + } + }); + + this.eventListeners[name] = newArray; +}; + +AudioRecorder.prototype.start = function() { + return new Promise(function(resolve, reject) { + this.onstart = function() { + this.onstart = null; + this.onerror = null; + resolve(1); + }.bind(this); + + this.onerror = function() { + this.onstart = null; + this.onerror = null; + resolve(0); + }.bind(this); + + this.sender({ type: "start" }); + }.bind(this)); +}; + +AudioRecorder.prototype.stop = function() { + return new Promise(function(resolve, reject) { + // To make sure the Player in Java can fetch data immediately, we + // need to return after data is back. + this.ondata = function ondata(message) { + _cleanEventListeners(); + + // The audio data we received are encoded with a proper format, it doesn't + // make sense to concatenate them like the socket, so let just override + // the buffered data here. + this.data = new Uint8Array(message.data); + resolve(1); + }.bind(this); + + var _onerror = function() { + _cleanEventListeners(); + resolve(0); + }.bind(this); + + var _cleanEventListeners = function() { + this.ondata = null; + this.removeEventListener("error", _onerror); + }.bind(this); + + this.addEventListener("error", _onerror); + this.sender({ type: "stop" }); + }.bind(this)); +}; + +AudioRecorder.prototype.pause = function() { + return new Promise(function(resolve, reject) { + // In Java, |stopRecord| might be called before |commit|, which triggers + // the calling sequence: + // nPause -> nGetRecordedSize -> nGetRecordedData -> nClose + // + // to make sure the Player in Java can fetch data in such a case, we + // need to request data immediately. + // + this.ondata = function ondata(message) { + this.ondata = null; + + // The audio data we received are encoded with a proper format, it doesn't + // make sense to concatenate them like the socket, so let just override + // the buffered data here. + this.data = new Uint8Array(message.data); + resolve(1); + }.bind(this); + + // Have to request data first before pausing. + this.requestData(); + this.sender({ type: "pause" }); + }.bind(this)); +}; + +AudioRecorder.prototype.requestData = function() { + this.sender({ type: "requestData" }); +}; + +AudioRecorder.prototype.close = function() { + if (this._closed) { + return new Promise(function(resolve) { resolve(1); }); } + + // Make sure recording is stopped on the other side. + return this.stop().then(function() { + DumbPipe.close(this.sender); + this._closed = true; + }.bind(this)); }; Native.create("com/sun/mmedia/PlayerImpl.nInit.(IILjava/lang/String;)I", function(appId, pId, jURI) { var url = util.fromJavaString(jURI); var id = pId + (appId << 32); - PlayerCache[id] = new Player(url); + Media.PlayerCache[id] = new PlayerContainer(url); return id; }); @@ -405,45 +878,47 @@ Native.create("com/sun/mmedia/PlayerImpl.nInit.(IILjava/lang/String;)I", functio * @return 0 - failed; 1 - succeeded. */ Native.create("com/sun/mmedia/PlayerImpl.nTerm.(I)I", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; if (!player) { return 1; } player.close(); - delete PlayerCache[handle]; + delete Media.PlayerCache[handle]; return 1; }); Native.create("com/sun/mmedia/PlayerImpl.nGetMediaFormat.(I)Ljava/lang/String;", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; player.mediaFormat = player.getMediaFormat(); return player.mediaFormat; }); +Native.create("com/sun/mmedia/DirectPlayer.nGetContentType.(I)Ljava/lang/String;", function(handle) { + return Media.PlayerCache[handle].getContentType(); +}); + Native.create("com/sun/mmedia/PlayerImpl.nIsHandledByDevice.(I)Z", function(handle) { - console.warn("com/sun/mmedia/PlayerImpl.nIsHandledByDevice.(I)Z not implemented"); - return false; + return Media.PlayerCache[handle].isHandledByDevice(); }); Native.create("com/sun/mmedia/PlayerImpl.nRealize.(ILjava/lang/String;)Z", function(handle, jMime) { var mime = util.fromJavaString(jMime); - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; return player.realize(mime); -}); - +}, true); Native.create("com/sun/mmedia/MediaDownload.nGetJavaBufferSize.(I)I", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; return player.getBufferSize(); }); Native.create("com/sun/mmedia/MediaDownload.nGetFirstPacketSize.(I)I", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; return player.getBufferSize() / 2; }); Native.create("com/sun/mmedia/MediaDownload.nBuffering.(I[BII)I", function(handle, buffer, offset, size) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; var bufferSize = player.getBufferSize(); // Check the parameters. @@ -458,12 +933,12 @@ Native.create("com/sun/mmedia/MediaDownload.nBuffering.(I[BII)I", function(handl }); Native.create("com/sun/mmedia/MediaDownload.nNeedMoreDataImmediatelly.(I)Z", function(handle) { - console.error("com/sun/mmedia/MediaDownload.nNeedMoreDataImmediatelly.(I)Z not implemented"); + console.warn("com/sun/mmedia/MediaDownload.nNeedMoreDataImmediatelly.(I)Z not implemented"); return true; }); Native.create("com/sun/mmedia/MediaDownload.nSetWholeContentSize.(IJ)V", function(handle, contentSize, _) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; player.wholeContentSize = contentSize.toNumber(); }); @@ -478,101 +953,175 @@ Native.create("com/sun/mmedia/DirectPlayer.nIsMIDIControlSupported.(I)Z", functi }); Native.create("com/sun/mmedia/DirectPlayer.nIsVideoControlSupported.(I)Z", function(handle) { - var player = PlayerCache[handle]; - console.warn("com/sun/mmedia/DirectPlayer.nIsVideoControlSupported.(I)Z not implemented."); - return false; + return Media.PlayerCache[handle].isVideoControlSupported(); }); Native.create("com/sun/mmedia/DirectPlayer.nIsVolumeControlSupported.(I)Z", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; return player.isVolumeControlSupported(); }); Native.create("com/sun/mmedia/DirectPlayer.nIsNeedBuffering.(I)Z", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; console.warn("com/sun/mmedia/DirectPlayer.nIsNeedBuffering.(I)Z not implemented."); return false; }); Native.create("com/sun/mmedia/DirectPlayer.nPcmAudioPlayback.(I)Z", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; console.warn("com/sun/mmedia/DirectPlayer.nPcmAudioPlayback.(I)Z not implemented."); return false; }); // Device is available? Native.create("com/sun/mmedia/DirectPlayer.nAcquireDevice.(I)Z", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; console.warn("com/sun/mmedia/DirectPlayer.nAcquireDevice.(I)Z not implemented."); return true; }); // Relase device reference Native.create("com/sun/mmedia/DirectPlayer.nReleaseDevice.(I)V", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; console.warn("com/sun/mmedia/DirectPlayer.nReleaseDevice.(I)V not implemented."); }); Native.create("com/sun/mmedia/DirectPlayer.nSwitchToForeground.(II)Z", function(handle, options) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; console.warn("com/sun/mmedia/DirectPlayer.nSwitchToForeground.(II)Z not implemented. "); return true; }); Native.create("com/sun/mmedia/DirectPlayer.nSwitchToBackground.(II)Z", function(handle, options) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; console.warn("com/sun/mmedia/DirectPlayer.nSwitchToBackground.(II)Z not implemented. "); return true; }); // Start Prefetch of Native Player Native.create("com/sun/mmedia/DirectPlayer.nPrefetch.(I)Z", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; console.warn("com/sun/mmedia/DirectPlayer.nPrefetch.(I)Z not implemented."); return true; }); Native.create("com/sun/mmedia/DirectPlayer.nGetMediaTime.(I)I", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; return player.getMediaTime(); }); Native.create("com/sun/mmedia/DirectPlayer.nStart.(I)Z", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; player.start(); return true; }); Native.create("com/sun/mmedia/DirectPlayer.nStop.(I)Z", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; player.close(); return true; }); Native.create("com/sun/mmedia/DirectPlayer.nTerm.(I)I", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; player.close(); - delete PlayerCache[handle]; + delete Media.PlayerCache[handle]; return 1; }); Native.create("com/sun/mmedia/DirectPlayer.nPause.(I)Z", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; player.pause(); return true; }); Native.create("com/sun/mmedia/DirectPlayer.nResume.(I)Z", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; player.resume(); return true; }); +Native.create("com/sun/mmedia/DirectPlayer.nGetWidth.(I)I", function(handle) { + return Media.PlayerCache[handle].getWidth(); +}); + +Native.create("com/sun/mmedia/DirectPlayer.nGetHeight.(I)I", function(handle) { + return Media.PlayerCache[handle].getHeight(); +}); + +Native.create("com/sun/mmedia/DirectPlayer.nSetLocation.(IIIII)Z", function(handle, x, y, w, h) { + Media.PlayerCache[handle].setLocation(x, y, w, h); + return true; +}); + +Native.create("com/sun/mmedia/DirectPlayer.nSetVisible.(IZ)Z", function(handle, visible) { + Media.PlayerCache[handle].setVisible(visible); + return true; +}); + +Native.create("com/sun/mmedia/DirectPlayer.nIsRecordControlSupported.(I)Z", function(handle) { + return !!(Media.PlayerCache[handle] && Media.PlayerCache[handle].audioRecorder); +}); + +Native.create("com/sun/mmedia/DirectPlayer.nGetDuration.(I)I", function(handle) { + return Media.PlayerCache[handle].getDuration(); +}) + +Native.create("com/sun/mmedia/DirectRecord.nSetLocator.(ILjava/lang/String;)I", function(handle, locator) { + console.warn("com/sun/mmedia/DirectRecord.nSetLocator.(I)I not implemented."); + return -1; +}); + +Native.create("com/sun/mmedia/DirectRecord.nGetRecordedSize.(I)I", function(handle) { + return Media.PlayerCache[handle].getRecordedSize(); +}); + +Native.create("com/sun/mmedia/DirectRecord.nGetRecordedData.(III[B)I", function(handle, offset, size, buffer) { + Media.PlayerCache[handle].getRecordedData(offset, size, buffer); + return 1; +}); + +Native.create("com/sun/mmedia/DirectRecord.nCommit.(I)I", function(handle) { + // In DirectRecord.java, before nCommit, nPause or nStop is called, + // which means all the recorded data has been fetched, so do nothing here. + return 1; +}); + +Native.create("com/sun/mmedia/DirectRecord.nPause.(I)I", function(handle) { + return Media.PlayerCache[handle].audioRecorder.pause(); +}, true); + +Native.create("com/sun/mmedia/DirectRecord.nStop.(I)I", function(handle) { + return Media.PlayerCache[handle].audioRecorder.stop(); +}, true); + +Native.create("com/sun/mmedia/DirectRecord.nClose.(I)I", function(handle) { + var player = Media.PlayerCache[handle]; + + if (!player || !player.audioRecorder) { + // We need to check if |audioRecorder| is still available, because |nClose| + // might be called twice in DirectRecord.java, and only IOException is + // handled in DirectRecord.java, let use IOException instead of IllegalStateException. + throw new JavaException("java/io/IOException"); + } + + return player.audioRecorder.close().then(function(result) { + delete player.audioRecorder; + return result; + }); +}, true); + +Native.create("com/sun/mmedia/DirectRecord.nStart.(I)I", function(handle) { + // In DirectRecord.java, nStart plays two roles: real start and resume. + // Let's handle this on the other side of the DumbPipe. + return Media.PlayerCache[handle].audioRecorder.start(); +}, true); + /** * @return the volume level between 0 and 100 if succeeded. Otherwise -1. */ Native.create("com/sun/mmedia/DirectVolume.nGetVolume.(I)I", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; return player.getVolume(); }); @@ -581,17 +1130,17 @@ Native.create("com/sun/mmedia/DirectVolume.nGetVolume.(I)I", function(handle) { * @return the volume level set between 0 and 100 if succeeded. Otherwise -1. */ Native.create("com/sun/mmedia/DirectVolume.nSetVolume.(II)I", function(handle, level) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; return player.setVolume(level); }); Native.create("com/sun/mmedia/DirectVolume.nIsMuted.(I)Z", function(handle) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; return player.getMute(); }); Native.create("com/sun/mmedia/DirectVolume.nSetMute.(IZ)Z", function(handle, mute) { - var player = PlayerCache[handle]; + var player = Media.PlayerCache[handle]; player.setMute(mute); return true; }); @@ -605,3 +1154,11 @@ Native.create("com/sun/mmedia/NativeTonePlayer.nStopTone.(I)Z", function(appId) console.warn("com/sun/mmedia/NativeTonePlayer.nStopTone.(I)Z not implemented."); return true; }); + +Native.create("com/sun/mmedia/DirectPlayer.nStartSnapshot.(ILjava/lang/String;)V", function(handle, imageType) { + Media.PlayerCache[handle].startSnapshot(util.fromJavaString(imageType)); +}); + +Native.create("com/sun/mmedia/DirectPlayer.nGetSnapshotData.(I)[B", function(handle) { + return Media.PlayerCache[handle].getSnapshotData(); +}); diff --git a/midp/midp.js b/midp/midp.js index 1ac554de4..8c558cf78 100644 --- a/midp/midp.js +++ b/midp/midp.js @@ -227,10 +227,22 @@ Native.create("com/sun/midp/security/Permissions.loadGroupPermissions.(Ljava/lan Native.create("com/sun/midp/main/CldcPlatformRequest.dispatchPlatformRequest.(Ljava/lang/String;)Z", function(request) { request = util.fromJavaString(request); if (request.startsWith("http://") || request.startsWith("https://")) { - window.open(request); + window.open(request); + } else if (request.startsWith("x-contacts:add?number=")) { + new MozActivity({ + name: "new", + data: { + type: "webcontacts/contact", + params: { + tel: request.substring(22), + }, + }, + }); } else { console.warn("com/sun/midp/main/CldcPlatformRequest.dispatchPlatformRequest.(Ljava/lang/String;)Z not implemented for: " + request); } + + return false; }); Native.create("com/sun/midp/main/CommandState.restoreCommandState.(Lcom/sun/midp/main/CommandState;)V", function(state) { @@ -421,7 +433,7 @@ Native.create("com/sun/midp/main/Configuration.getProperty0.(Ljava/lang/String;) value = null; break; } - return value ? value : null; + return value; }); Native.create("com/sun/midp/chameleon/skins/resources/LoadedSkinData.beginReadingSkinFile.(Ljava/lang/String;)V", function(fileName) { @@ -610,14 +622,23 @@ MIDP.Context2D = (function() { // both. A distance threshold ensures that touches with an "intent // to tap" will likely result in a tap. + var LONG_PRESS_TIMEOUT = 1000; var MIN_DRAG_DISTANCE_SQUARED = 5 * 5; var mouseDownInfo = null; + var longPressTimeoutID = null; + var longPressDetected = false; c.addEventListener(supportsTouch ? "touchstart" : "mousedown", function(event) { event.preventDefault(); // Prevent unnecessary fake mouse events. var pt = getEventPoint(event); sendPenEvent(pt, MIDP.PRESSED); mouseDownInfo = pt; + + longPressDetected = false; + longPressTimeoutID = setTimeout(function() { + longPressDetected = true; + sendGestureEvent(pt, null, MIDP.GESTURE_LONG_PRESS); + }, LONG_PRESS_TIMEOUT); }); c.addEventListener(supportsTouch ? "touchmove" : "mousemove", function(event) { @@ -625,6 +646,12 @@ MIDP.Context2D = (function() { return; // Mousemove on desktop; ignored. } event.preventDefault(); + + if (longPressTimeoutID) { + clearTimeout(longPressTimeoutID); + longPressTimeoutID = null; + } + var pt = getEventPoint(event); sendPenEvent(pt, MIDP.DRAGGED); var distance = { @@ -639,7 +666,9 @@ MIDP.Context2D = (function() { mouseDownInfo.isDragging = true; mouseDownInfo.x = pt.x; mouseDownInfo.y = pt.y; - sendGestureEvent(pt, distance, MIDP.GESTURE_DRAG); + if (!longPressDetected) { + sendGestureEvent(pt, distance, MIDP.GESTURE_DRAG); + } } }); @@ -650,9 +679,17 @@ MIDP.Context2D = (function() { } event.preventDefault(); + if (longPressTimeoutID) { + clearTimeout(longPressTimeoutID); + longPressTimeoutID = null; + } + var pt = getEventPoint(event); sendPenEvent(pt, MIDP.RELEASED); - sendGestureEvent(pt, null, mouseDownInfo.isDragging ? MIDP.GESTURE_DROP : MIDP.GESTURE_TAP); + + if (!longPressDetected) { + sendGestureEvent(pt, null, mouseDownInfo.isDragging ? MIDP.GESTURE_DROP : MIDP.GESTURE_TAP); + } mouseDownInfo = null; // Clear the way for the next gesture. }); @@ -855,6 +892,7 @@ MIDP.RELEASED = 2; MIDP.DRAGGED = 3; MIDP.COMMAND_EVENT = 3; MIDP.EVENT_QUEUE_SHUTDOWN = 31; +MIDP.MMAPI_EVENT = 45; MIDP.GESTURE_EVENT = 71; MIDP.GESTURE_TAP = 0x1; MIDP.GESTURE_LONG_PRESS = 0x2; diff --git a/midp/sms.js b/midp/sms.js index b8d1a18bd..8bf1cd9a7 100644 --- a/midp/sms.js +++ b/midp/sms.js @@ -75,14 +75,14 @@ Native.create("com/sun/midp/io/j2me/sms/Protocol.open0.(Ljava/lang/String;II)I", host: util.fromJavaString(host), }; - promptForMessageText(); - return ++MIDP.lastSMSConnection; }); Native.create("com/sun/midp/io/j2me/sms/Protocol.receive0.(IIILcom/sun/midp/io/j2me/sms/Protocol$SMSPacket;)I", function(port, msid, handle, smsPacket) { return new Promise(function(resolve, reject) { + promptForMessageText(); + function receiveSMS() { var sms = MIDP.j2meSMSMessages.shift(); var text = sms.text; @@ -122,3 +122,30 @@ Native.create("com/sun/midp/io/j2me/sms/Protocol.close0.(III)I", function(port, delete MIDP.smsConnections[handle]; return 0; }); + +Native.create("com/sun/midp/io/j2me/sms/Protocol.numberOfSegments0.([BIIZ)I", function(msgBuffer, msgLen, msgType, hasPort) { + console.warn("com/sun/midp/io/j2me/sms/Protocol.numberOfSegments0.([BIIZ)I not implemented"); + return 1; +}); + +Native.create("com/sun/midp/io/j2me/sms/Protocol.send0.(IILjava/lang/String;II[B)I", +function(handle, type, host, destPort, sourcePort, message) { + return new Promise(function(resolve, reject) { + var activity = new MozActivity({ + name: "new", + data: { + type: "websms/sms", + number: util.fromJavaString(host), + body: new TextDecoder('utf-16be').decode(message), + }, + }); + + activity.onsuccess = function() { + resolve(message.byteLength); + }; + + activity.onerror = function() { + reject(new JavaException("java/io/IOException", "Error while sending SMS message")); + }; + }); +}, true); diff --git a/midp/socket.js b/midp/socket.js index 94c01c0c8..baa9e382f 100644 --- a/midp/socket.js +++ b/midp/socket.js @@ -80,9 +80,9 @@ Native.create("com/sun/midp/io/j2me/socket/Protocol.open0.([BI)V", function(ipBy this.socket.ondata = (function(message) { // console.log("this.socket.ondata: " + JSON.stringify(message)); - var newArray = new Uint8Array(this.data.byteLength + message.data.length); + var newArray = new Uint8Array(this.data.byteLength + message.data.byteLength); newArray.set(this.data); - newArray.set(message.data, this.data.byteLength); + newArray.set(new Uint8Array(message.data), this.data.byteLength); this.data = newArray; if (this.waitingData) { diff --git a/native.js b/native.js index ad10d5dca..96c89b38d 100644 --- a/native.js +++ b/native.js @@ -70,7 +70,7 @@ Native.create("java/lang/System.getProperty0.(Ljava/lang/String;)Ljava/lang/Stri value = urlParams.platform ? urlParams.platform : "NOKIA503/JAVA_RUNTIME_VERSION=NOKIA_ASHA_1_2"; break; case "microedition.platformimpl": - value = ""; + value = null; break; case "microedition.profiles": value = "MIDP-2.0" @@ -81,6 +81,9 @@ Native.create("java/lang/System.getProperty0.(Ljava/lang/String;)Ljava/lang/Stri case "microedition.amms.version": value = "1.1"; break; + case "microedition.media.version": + value = '1.2'; + break; case "mmapi-configuration": value = null; break; @@ -148,6 +151,10 @@ Native.create("java/lang/System.getProperty0.(Ljava/lang/String;)Ljava/lang/Stri console.warn("Property 'com.nokia.multisim.imsi.sim2' is a stub"); value = null; break; + case "com.nokia.mid.batterylevel": + // http://developer.nokia.com/community/wiki/Checking_battery_level_in_Java_ME + value = Math.floor(navigator.battery.level * 100).toString(); + break; case "com.nokia.mid.imsi": console.warn("Property 'com.nokia.mid.imsi' is a stub"); value = "000000000000000"; @@ -175,11 +182,24 @@ Native.create("java/lang/System.getProperty0.(Ljava/lang/String;)Ljava/lang/Stri case "classpathext": value = null; break; + case "supports.audio.capture": + value = "true"; + break; + case "supports.recording": + value = "true"; + break; + case "audio.encodings": + value = "audio/ogg"; + break; + case "video.snapshot.encodings": + value = "encoding=jpeg"; + break; default: console.warn("UNKNOWN PROPERTY (java/lang/System): " + util.fromJavaString(key)); + value = null; break; } - return value ? value : null; + return value; }); Native.create("java/lang/System.currentTimeMillis.()J", function() { @@ -556,6 +576,13 @@ Native.create("com/sun/cldc/io/ResourceInputStream.open.(Ljava/lang/String;)Ljav return obj; }); +Native.create("com/sun/cldc/io/ResourceInputStream.clone.(Ljava/lang/Object;)Ljava/lang/Object;", function(source) { + var obj = util.newObject(CLASSES.java_lang_Object); + obj.data = new Uint8Array(source.data); + obj.pos = source.pos; + return obj; +}); + Override.create("com/sun/cldc/io/ResourceInputStream.available.()I", function() { var handle = this.$fileDecoder; @@ -738,6 +765,47 @@ Native.create("java/io/DataInputStream.bytesToUTF.([B)Ljava/lang/String;", funct } }); +Native.create("java/io/DataOutputStream.UTFToBytes.(Ljava/lang/String;)[B", function(jStr) { + var str = util.fromJavaString(jStr); + + var utflen = 0; + + for (var i = 0; i < str.length; i++) { + var c = str.charCodeAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) { + utflen++; + } else if (c > 0x07FF) { + utflen += 3; + } else { + utflen += 2; + } + } + + if (utflen > 65535) { + throw new JavaException("java/io/UTFDataFormatException"); + } + + var count = 0; + var bytearr = util.newPrimitiveArray("B", utflen + 2); + bytearr[count++] = (utflen >>> 8) & 0xFF; + bytearr[count++] = (utflen >>> 0) & 0xFF; + for (var i = 0; i < str.length; i++) { + var c = str.charCodeAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) { + bytearr[count++] = c; + } else if (c > 0x07FF) { + bytearr[count++] = 0xE0 | ((c >> 12) & 0x0F); + bytearr[count++] = 0x80 | ((c >> 6) & 0x3F); + bytearr[count++] = 0x80 | ((c >> 0) & 0x3F); + } else { + bytearr[count++] = 0xC0 | ((c >> 6) & 0x1F); + bytearr[count++] = 0x80 | ((c >> 0) & 0x3F); + } + } + + return bytearr; +}); + Native.create("com/sun/cldc/i18n/j2me/UTF_8_Writer.encodeUTF8.([CII)[B", function(cbuf, off, len) { var outputArray = []; diff --git a/tests/TestFileSystemPerf.java b/tests/TestFileSystemPerf.java new file mode 100644 index 000000000..e750c000a --- /dev/null +++ b/tests/TestFileSystemPerf.java @@ -0,0 +1,62 @@ +import javax.microedition.io.*; +import javax.microedition.io.file.*; +import java.io.*; + +public class TestFileSystemPerf { + public static void main(String args[]) { + try { + String dirPath = System.getProperty("fileconn.dir.private"); + long then; + + String str = "I am the very model of a modern major general."; + byte[] bytes = str.getBytes(); + + then = System.currentTimeMillis(); + FileConnection file = (FileConnection)Connector.open(dirPath + "test.txt"); + System.out.println("Time to open file: " + (System.currentTimeMillis() - then) + "ms"); + + then = System.currentTimeMillis(); + file.create(); + System.out.println("Time to create file: " + (System.currentTimeMillis() - then) + "ms"); + + then = System.currentTimeMillis(); + OutputStream out = file.openOutputStream(); + for (int i = 0; i < 1000; i++) { + out.write(bytes); + out.flush(); + } + System.out.println("Time to write/flush to output stream: " + (System.currentTimeMillis() - then) + "ms"); + + then = System.currentTimeMillis(); + out.close(); + System.out.println("Time to close output stream: " + (System.currentTimeMillis() - then) + "ms"); + + then = System.currentTimeMillis(); + file.delete(); + System.out.println("Time to delete file: " + (System.currentTimeMillis() - then) + "ms"); + + then = System.currentTimeMillis(); + file.close(); + System.out.println("Time to close file: " + (System.currentTimeMillis() - then) + "ms"); + + then = System.currentTimeMillis(); + file = (FileConnection)Connector.open(dirPath + "test.txt"); + System.out.println("Time to reopen file: " + (System.currentTimeMillis() - then) + "ms"); + + then = System.currentTimeMillis(); + file = (FileConnection)Connector.open(dirPath + "test2.txt"); + file.create(); + out = file.openOutputStream(); + out.write(bytes); + out.flush(); + out.close(); + file.delete(); + file.close(); + System.out.println("Time to access another file: " + (System.currentTimeMillis() - then) + "ms"); + + } catch (Exception e) { + System.out.println("Unexpected exception: " + e); + e.printStackTrace(); + } + } +} diff --git a/tests/automation.js b/tests/automation.js index 655fd8c60..aadc598e0 100644 --- a/tests/automation.js +++ b/tests/automation.js @@ -17,7 +17,7 @@ casper.options.onWaitTimeout = function() { var gfxTests = [ { name: "gfx/AlertTest", maxDifferent: 1621 }, - { name: "gfx/CanvasTest", maxDifferent: 269 }, + { name: "gfx/CanvasTest", maxDifferent: 271 }, { name: "gfx/DrawRegionTest", maxDifferent: 0 }, { name: "gfx/ImageRenderingTest", maxDifferent: 266 }, { name: "gfx/FillRectTest", maxDifferent: 0 }, @@ -38,18 +38,40 @@ var gfxTests = [ { name: "gfx/ImageProcessingTest", maxDifferent: 6184 }, { name: "gfx/CreateImageWithRegionTest", maxDifferent: 0 }, { name: "gfx/DrawSubstringTest", maxDifferent: 332 }, + { name: "gfx/DrawLineOffscreenCanvasTest", maxDifferent: 0 }, +]; + +var expectedUnitTestResults = [ + { name: "pass", number: 71145 }, + { name: "fail", number: 0 }, + { name: "known fail", number: 180 }, + { name: "unknown pass", number: 0 } ]; casper.test.begin("unit tests", 7 + gfxTests.length, function(test) { function basicUnitTests() { casper.waitForText("DONE", function() { var content = this.getPageContent(); - if (content.contains("DONE: 71088 pass, 0 fail, 179 known fail, 0 unknown pass")) { - test.pass('main unit tests'); + var regex = /DONE: (\d+) pass, (\d+) fail, (\d+) known fail, (\d+) unknown pass/; + var match = content.match(regex); + if (!match || !match.length || match.length < 5) { + this.debugPage(); + this.echo(this.captureBase64('png')); + test.fail('failed to parse status line of main unit tests'); } else { - this.debugPage(); - this.echo(this.captureBase64('png')); - test.fail('main unit tests'); + var msg = ""; + for (var i = 0; i < expectedUnitTestResults.length; i++) { + if (match[i+1] != expectedUnitTestResults[i].number) { + msg += "\n\tExpected " + expectedUnitTestResults[i].number + " " + expectedUnitTestResults[i].name + ". Got " + match[i+1]; + } + } + if (!msg) { + test.pass('main unit tests'); + } else { + this.debugPage(); + this.echo(this.captureBase64('png')); + test.fail(msg); + } } }); }); diff --git a/tests/com/ibm/oti/connection/file/TestFileConnection.java b/tests/com/ibm/oti/connection/file/TestFileConnection.java index 12c9d1956..e16d29907 100644 --- a/tests/com/ibm/oti/connection/file/TestFileConnection.java +++ b/tests/com/ibm/oti/connection/file/TestFileConnection.java @@ -149,6 +149,47 @@ void testLastModified(TestHarness th) throws IOException { file.close(); } + class CreateFileThread extends Thread { + String name; + TestHarness th; + public CreateFileThread(String name, TestHarness th) { + this.name = name; + this.th = th; + } + + public void run() { + try { + ((FileConnection)Connector.open(name)).create(); + } catch (IOException ioe) { + th.fail("Unexpected failure."); + ioe.printStackTrace(); + } + } + } + + class RemoveFileThread extends Thread { + String name; + TestHarness th; + + public RemoveFileThread(String name, TestHarness th) { + this.name = name; + this.th = th; + } + + public void run() { + try { + FileConnection file = (FileConnection)Connector.open(name); + th.check(file.exists(), name + " exists."); + file.delete(); + th.check(!file.exists(), name + " has been removed."); + file.close(); + } catch (IOException ioe) { + th.fail("Unexpected failure."); + ioe.printStackTrace(); + } + } + } + public void test(TestHarness th) { try { dirPath = System.getProperty("fileconn.dir.private"); @@ -395,7 +436,44 @@ public void test(TestHarness th) { file.delete(); file.close(); + // Check creating files in multi-thread + Hashtable expected = new Hashtable(); + expected.put("ThreadFile1", ""); + expected.put("ThreadFile2", ""); + expected.put("ThreadFile3", ""); + + Thread t1 = new CreateFileThread(dirPath + "provaDir/" + "ThreadFile1", th); + Thread t2 = new CreateFileThread(dirPath + "provaDir/" + "ThreadFile2", th); + Thread t3 = new CreateFileThread(dirPath + "provaDir/" + "ThreadFile3", th); + t1.start(); + t2.start(); + t3.start(); + t1.join(); + t2.join(); + t3.join(); + dir = (FileConnection)Connector.open(dirPath + "provaDir"); + files = dir.list(); + while (files.hasMoreElements()) { + String fileName = (String)files.nextElement(); + th.check(expected.remove(fileName) != null); + } + th.check(expected.isEmpty(), "3 files were created and removed."); + + // Check removing files in multi-thread + t1 = new RemoveFileThread(dirPath + "provaDir/" + "ThreadFile1", th); + t2 = new RemoveFileThread(dirPath + "provaDir/" + "ThreadFile2", th); + t3 = new RemoveFileThread(dirPath + "provaDir/" + "ThreadFile3", th); + t1.start(); + t2.start(); + t3.start(); + t1.join(); + t2.join(); + t3.join(); + + files = dir.list(); + th.check(!files.hasMoreElements(), "dir is empty."); + dir.delete(); th.check(!dir.exists()); dir.close(); diff --git a/tests/com/nokia/mid/ui/TestDeviceControl.java b/tests/com/nokia/mid/ui/TestDeviceControl.java new file mode 100644 index 000000000..da39d4353 --- /dev/null +++ b/tests/com/nokia/mid/ui/TestDeviceControl.java @@ -0,0 +1,46 @@ +package com.nokia.mid.ui; + +import gnu.testlet.TestHarness; +import gnu.testlet.Testlet; + +public class TestDeviceControl implements Testlet { + public void test(TestHarness th) { + try { + DeviceControl.stopVibra(); + + DeviceControl.startVibra(0, 0); + DeviceControl.stopVibra(); + + try { + DeviceControl.startVibra(-1, 100); + th.fail("IllegalArgumentException expected"); + } catch (IllegalArgumentException e) { + th.check(true); + } + + try { + DeviceControl.startVibra(200, 100); + th.fail("IllegalArgumentException expected"); + } catch (IllegalArgumentException e) { + th.check(true); + } + + try { + DeviceControl.startVibra(50, -1); + th.fail("IllegalArgumentException expected"); + } catch (IllegalArgumentException e) { + th.check(true); + } + + DeviceControl.startVibra(100, 100); + Thread.sleep(50); + DeviceControl.stopVibra(); + + // All done. + th.check(true); + } catch (Exception e) { + th.fail("Unexpected exception: " + e); + e.printStackTrace(); + } + } +} diff --git a/tests/com/nokia/mid/ui/TestVirtualKeyboard.java b/tests/com/nokia/mid/ui/TestVirtualKeyboard.java new file mode 100644 index 000000000..c95493edd --- /dev/null +++ b/tests/com/nokia/mid/ui/TestVirtualKeyboard.java @@ -0,0 +1,118 @@ +/* vim: set filetype=java shiftwidth=4 tabstop=4 autoindent cindent expandtab : */ + +package com.nokia.mid.ui; + +import gnu.testlet.TestHarness; +import gnu.testlet.Testlet; +import javax.microedition.lcdui.Canvas; +import javax.microedition.lcdui.Font; +import javax.microedition.lcdui.Graphics; +import com.nokia.mid.ui.VirtualKeyboard; + +class TestKeyboardVisibilityListener implements com.nokia.mid.ui.KeyboardVisibilityListener { + boolean isExpectingShow = false; + boolean isExpectingHide = false; + TestHarness th; + + TestKeyboardVisibilityListener(TestHarness harness) { + th = harness; + } + + // Making this method `synchronized` caused an + // `IllegalMonitorStateException` to be thrown. Using a + // `synchronized(this)` statement around the method body + // as a workaround. + public void showNotify(int keyboardCategory) { + synchronized(this) { + th.check(isExpectingShow, true); + isExpectingShow = false; + TestVirtualKeyboard.verifyKeyboardShown(th); + notify(); + } + } + + // Making this method `synchronized` caused an + // `IllegalMonitorStateException` to be thrown. Using a + // `synchronized(this)` statement around the method body + // as a workaround. + public void hideNotify(int keyboardCategory) { + synchronized(this) { + th.check(isExpectingHide, true); + isExpectingHide = false; + TestVirtualKeyboard.verifyKeyboardHidden(th); + notify(); + } + } +} + +public class TestVirtualKeyboard extends Canvas implements Testlet { + public native static void hideKeyboard(); + public native static void showKeyboard(); + + public static void verifyKeyboardHidden(TestHarness th) { + th.check(VirtualKeyboard.isVisible(), false); + + // We may consider checking these values but it's not clear from + // documentation what we should expect them to be + // th.check(VirtualKeyboard.getXPosition(), 0); + // th.check(VirtualKeyboard.getYPosition(), /* Height of window */); + // th.check(VirtualKeyboard.getWidth(), 0); + // th.check(VirtualKeyboard.getHeight(), 0); + } + + public static void verifyKeyboardShown(TestHarness th) { + th.check(VirtualKeyboard.isVisible(), true); + th.check(VirtualKeyboard.getXPosition(), 0); + // th.check(VirtualKeyboard.getYPosition() + VirtualKeyboard.getHeight(), /* Window height */); + // th.check(VirtualKeyboard.getWidth(), /* Width of window */); + + // It would be nice to verify these values, but it's not clear + // what they should be (probably varies across devices) + // th.check(VirtualKeyboard.getYPosition(), /* unknown */); + // th.check(VirtualKeyboard.getHeight(), /* unknown */); + } + + public void test(TestHarness th) { + TestKeyboardVisibilityListener listener = new TestKeyboardVisibilityListener(th); + th.check(null == listener, false); + VirtualKeyboard.setVisibilityListener(listener); + + verifyKeyboardHidden(th); + + // These are separate `synchronized` sections in case there are + // notifications pending for the listener. We want them to be + // processed between calls to `wait`. + + // Test making the keyboard visible + synchronized(listener) { + listener.isExpectingShow = true; + showKeyboard(); + while(true) { + try { + listener.wait(); + break; + } catch (InterruptedException e) { + continue; + } + } + } + + // Test hiding the keyboard + synchronized(listener) { + listener.isExpectingHide = true; + hideKeyboard(); + while (true) { + try { + listener.wait(); + break; + } catch (InterruptedException e) { + continue; + } + } + } + + VirtualKeyboard.setVisibilityListener(null); + } + + protected void paint(Graphics graphics) {} +} diff --git a/tests/com/sun/cldc/io/TestResourceInputStream.java b/tests/com/sun/cldc/io/TestResourceInputStream.java index cd7bf3e59..407d9f243 100644 --- a/tests/com/sun/cldc/io/TestResourceInputStream.java +++ b/tests/com/sun/cldc/io/TestResourceInputStream.java @@ -6,6 +6,8 @@ import java.io.*; public class TestResourceInputStream implements Testlet { + TestHarness th; + private String readLine(InputStreamReader reader) throws IOException { // Test whether the end of file has been reached. If so, return null. int readChar = reader.read(); @@ -29,11 +31,21 @@ private String readLine(InputStreamReader reader) throws IOException { return string.toString(); } + public void readWithStreamReader(ResourceInputStream stream) throws IOException { + InputStreamReader reader = new InputStreamReader(stream); + th.check(readLine(reader), "ξεσκεπάζω τὴν ψυχοφθόρα βδελυγμία"); + th.check(readLine(reader) == null); + } + public void test(TestHarness th) { + this.th = th; + try { - InputStreamReader reader = new InputStreamReader(getClass().getResourceAsStream("utf8.txt")); - th.check(readLine(reader), "ξεσκεπάζω τὴν ψυχοφθόρα βδελυγμία"); - th.check(readLine(reader) == null); + ResourceInputStream stream = (ResourceInputStream)getClass().getResourceAsStream("utf8.txt"); + stream.mark(0); + readWithStreamReader(stream); + stream.reset(); + readWithStreamReader(stream); } catch (Exception e) { th.fail("Unexpected exception: " + e); e.printStackTrace(); diff --git a/tests/fstests.html b/tests/fstests.html index 2e252a40d..32fbb66e2 100644 --- a/tests/fstests.html +++ b/tests/fstests.html @@ -4,7 +4,6 @@ -