Return-Path: X-Original-To: apmail-incubator-callback-commits-archive@minotaur.apache.org Delivered-To: apmail-incubator-callback-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 4C0A29C5A for ; Fri, 3 Feb 2012 15:44:18 +0000 (UTC) Received: (qmail 51481 invoked by uid 500); 3 Feb 2012 15:44:18 -0000 Delivered-To: apmail-incubator-callback-commits-archive@incubator.apache.org Received: (qmail 51451 invoked by uid 500); 3 Feb 2012 15:44:17 -0000 Mailing-List: contact callback-commits-help@incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: callback-dev@incubator.apache.org Delivered-To: mailing list callback-commits@incubator.apache.org Received: (qmail 51443 invoked by uid 99); 3 Feb 2012 15:44:17 -0000 Received: from nike.apache.org (HELO nike.apache.org) (192.87.106.230) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 03 Feb 2012 15:44:17 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=5.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.114] (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 03 Feb 2012 15:43:57 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id 6B7E731B887; Fri, 3 Feb 2012 15:43:10 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: macdonst@apache.org To: callback-commits@incubator.apache.org X-Mailer: ASF-Git Admin Mailer Subject: [9/15] Rename to Cordova Message-Id: <20120203154310.6B7E731B887@tyr.zones.apache.org> Date: Fri, 3 Feb 2012 15:43:10 +0000 (UTC) X-Virus-Checked: Checked by ClamAV on apache.org http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/664a061d/framework/src/com/phonegap/file/InvalidModificationException.java ---------------------------------------------------------------------- diff --git a/framework/src/com/phonegap/file/InvalidModificationException.java b/framework/src/com/phonegap/file/InvalidModificationException.java deleted file mode 100644 index 505bd4d..0000000 --- a/framework/src/com/phonegap/file/InvalidModificationException.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - - -package com.phonegap.file; - -public class InvalidModificationException extends Exception { - - public InvalidModificationException(String message) { - super(message); - } - -} http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/664a061d/framework/src/com/phonegap/file/NoModificationAllowedException.java ---------------------------------------------------------------------- diff --git a/framework/src/com/phonegap/file/NoModificationAllowedException.java b/framework/src/com/phonegap/file/NoModificationAllowedException.java deleted file mode 100644 index f62cf06..0000000 --- a/framework/src/com/phonegap/file/NoModificationAllowedException.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -package com.phonegap.file; - -public class NoModificationAllowedException extends Exception { - - public NoModificationAllowedException(String message) { - super(message); - } - -} http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/664a061d/framework/src/com/phonegap/file/TypeMismatchException.java ---------------------------------------------------------------------- diff --git a/framework/src/com/phonegap/file/TypeMismatchException.java b/framework/src/com/phonegap/file/TypeMismatchException.java deleted file mode 100644 index 99e82c6..0000000 --- a/framework/src/com/phonegap/file/TypeMismatchException.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - - -package com.phonegap.file; - -public class TypeMismatchException extends Exception { - - public TypeMismatchException(String message) { - super(message); - } - -} http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/664a061d/framework/src/org/apache/cordova/AccelListener.java ---------------------------------------------------------------------- diff --git a/framework/src/org/apache/cordova/AccelListener.java b/framework/src/org/apache/cordova/AccelListener.java new file mode 100755 index 0000000..61d7acf --- /dev/null +++ b/framework/src/org/apache/cordova/AccelListener.java @@ -0,0 +1,308 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova; + +import java.util.List; + +import org.apache.cordova.api.CordovaActivity; +import org.apache.cordova.api.Plugin; +import org.apache.cordova.api.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + + +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.content.Context; + +/** + * This class listens to the accelerometer sensor and stores the latest + * acceleration values x,y,z. + */ +public class AccelListener extends Plugin implements SensorEventListener { + + public static int STOPPED = 0; + public static int STARTING = 1; + public static int RUNNING = 2; + public static int ERROR_FAILED_TO_START = 3; + + public float TIMEOUT = 30000; // Timeout in msec to shut off listener + + float x,y,z; // most recent acceleration values + long timestamp; // time of most recent value + int status; // status of listener + long lastAccessTime; // time the value was last retrieved + + private SensorManager sensorManager;// Sensor manager + Sensor mSensor; // Acceleration sensor returned by sensor manager + + /** + * Create an accelerometer listener. + */ + public AccelListener() { + this.x = 0; + this.y = 0; + this.z = 0; + this.timestamp = 0; + this.setStatus(AccelListener.STOPPED); + } + + /** + * Sets the context of the Command. This can then be used to do things like + * get file paths associated with the Activity. + * + * @param ctx The context of the main Activity. + */ + public void setContext(CordovaActivity ctx) { + super.setContext(ctx); + this.sensorManager = (SensorManager) ctx.getSystemService(Context.SENSOR_SERVICE); + } + + /** + * Executes the request and returns PluginResult. + * + * @param action The action to execute. + * @param args JSONArry of arguments for the plugin. + * @param callbackId The callback id used when calling back into JavaScript. + * @return A PluginResult object with a status and message. + */ + public PluginResult execute(String action, JSONArray args, String callbackId) { + PluginResult.Status status = PluginResult.Status.OK; + String result = ""; + + try { + if (action.equals("getStatus")) { + int i = this.getStatus(); + return new PluginResult(status, i); + } + else if (action.equals("start")) { + int i = this.start(); + return new PluginResult(status, i); + } + else if (action.equals("stop")) { + this.stop(); + return new PluginResult(status, 0); + } + else if (action.equals("getAcceleration")) { + // If not running, then this is an async call, so don't worry about waiting + if (this.status != AccelListener.RUNNING) { + int r = this.start(); + if (r == AccelListener.ERROR_FAILED_TO_START) { + return new PluginResult(PluginResult.Status.IO_EXCEPTION, AccelListener.ERROR_FAILED_TO_START); + } + // Wait until running + long timeout = 2000; + while ((this.status == STARTING) && (timeout > 0)) { + timeout = timeout - 100; + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + if (timeout == 0) { + return new PluginResult(PluginResult.Status.IO_EXCEPTION, AccelListener.ERROR_FAILED_TO_START); + } + } + this.lastAccessTime = System.currentTimeMillis(); + JSONObject r = new JSONObject(); + r.put("x", this.x); + r.put("y", this.y); + r.put("z", this.z); + // TODO: Should timestamp be sent? + r.put("timestamp", this.timestamp); + return new PluginResult(status, r); + } + else if (action.equals("setTimeout")) { + try { + float timeout = Float.parseFloat(args.getString(0)); + this.setTimeout(timeout); + return new PluginResult(status, 0); + } catch (NumberFormatException e) { + status = PluginResult.Status.INVALID_ACTION; + e.printStackTrace(); + } catch (JSONException e) { + status = PluginResult.Status.JSON_EXCEPTION; + e.printStackTrace(); + } + } + else if (action.equals("getTimeout")) { + float f = this.getTimeout(); + return new PluginResult(status, f); + } + return new PluginResult(status, result); + } catch (JSONException e) { + return new PluginResult(PluginResult.Status.JSON_EXCEPTION); + } + } + + /** + * Identifies if action to be executed returns a value and should be run synchronously. + * + * @param action The action to execute + * @return T=returns value + */ + public boolean isSynch(String action) { + if (action.equals("getStatus")) { + return true; + } + else if (action.equals("getAcceleration")) { + // Can only return value if RUNNING + if (this.status == RUNNING) { + return true; + } + } + else if (action.equals("getTimeout")) { + return true; + } + return false; + } + + /** + * Called by AccelBroker when listener is to be shut down. + * Stop listener. + */ + public void onDestroy() { + this.stop(); + } + + //-------------------------------------------------------------------------- + // LOCAL METHODS + //-------------------------------------------------------------------------- + + /** + * Start listening for acceleration sensor. + * + * @return status of listener + */ + public int start() { + + // If already starting or running, then just return + if ((this.status == AccelListener.RUNNING) || (this.status == AccelListener.STARTING)) { + return this.status; + } + + // Get accelerometer from sensor manager + List list = this.sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER); + + // If found, then register as listener + if ((list != null) && (list.size() > 0)) { + this.mSensor = list.get(0); + this.sensorManager.registerListener(this, this.mSensor, SensorManager.SENSOR_DELAY_FASTEST); + this.setStatus(AccelListener.STARTING); + this.lastAccessTime = System.currentTimeMillis(); + } + + // If error, then set status to error + else { + this.setStatus(AccelListener.ERROR_FAILED_TO_START); + } + + return this.status; + } + + /** + * Stop listening to acceleration sensor. + */ + public void stop() { + if (this.status != AccelListener.STOPPED) { + this.sensorManager.unregisterListener(this); + } + this.setStatus(AccelListener.STOPPED); + } + + /** + * Called when the accuracy of the sensor has changed. + * + * @param sensor + * @param accuracy + */ + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + /** + * Sensor listener event. + * + * @param SensorEvent event + */ + public void onSensorChanged(SensorEvent event) { + + // Only look at accelerometer events + if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) { + return; + } + + // If not running, then just return + if (this.status == AccelListener.STOPPED) { + return; + } + + // Save time that event was received + this.timestamp = System.currentTimeMillis(); + this.x = event.values[0]; + this.y = event.values[1]; + this.z = event.values[2]; + + this.setStatus(AccelListener.RUNNING); + + // If values haven't been read for TIMEOUT time, then turn off accelerometer sensor to save power + if ((this.timestamp - this.lastAccessTime) > this.TIMEOUT) { + this.stop(); + } + } + + /** + * Get status of accelerometer sensor. + * + * @return status + */ + public int getStatus() { + return this.status; + } + + /** + * Set the timeout to turn off accelerometer sensor if getX() hasn't been called. + * + * @param timeout Timeout in msec. + */ + public void setTimeout(float timeout) { + this.TIMEOUT = timeout; + } + + /** + * Get the timeout to turn off accelerometer sensor if getX() hasn't been called. + * + * @return timeout in msec + */ + public float getTimeout() { + return this.TIMEOUT; + } + + /** + * Set the status and send it to JavaScript. + * @param status + */ + private void setStatus(int status) { + this.status = status; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/664a061d/framework/src/org/apache/cordova/App.java ---------------------------------------------------------------------- diff --git a/framework/src/org/apache/cordova/App.java b/framework/src/org/apache/cordova/App.java new file mode 100755 index 0000000..9b1c96a --- /dev/null +++ b/framework/src/org/apache/cordova/App.java @@ -0,0 +1,198 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +package org.apache.cordova; + +import org.apache.cordova.api.LOG; +import org.apache.cordova.api.Plugin; +import org.apache.cordova.api.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import java.util.HashMap; + +/** + * This class exposes methods in DroidGap that can be called from JavaScript. + */ +public class App extends Plugin { + + /** + * Executes the request and returns PluginResult. + * + * @param action The action to execute. + * @param args JSONArry of arguments for the plugin. + * @param callbackId The callback id used when calling back into JavaScript. + * @return A PluginResult object with a status and message. + */ + public PluginResult execute(String action, JSONArray args, String callbackId) { + PluginResult.Status status = PluginResult.Status.OK; + String result = ""; + + try { + if (action.equals("clearCache")) { + this.clearCache(); + } + else if (action.equals("loadUrl")) { + this.loadUrl(args.getString(0), args.optJSONObject(1)); + } + else if (action.equals("cancelLoadUrl")) { + this.cancelLoadUrl(); + } + else if (action.equals("clearHistory")) { + this.clearHistory(); + } + else if (action.equals("backHistory")) { + this.backHistory(); + } + else if (action.equals("overrideBackbutton")) { + this.overrideBackbutton(args.getBoolean(0)); + } + else if (action.equals("isBackbuttonOverridden")) { + boolean b = this.isBackbuttonOverridden(); + return new PluginResult(status, b); + } + else if (action.equals("exitApp")) { + this.exitApp(); + } + return new PluginResult(status, result); + } catch (JSONException e) { + return new PluginResult(PluginResult.Status.JSON_EXCEPTION); + } + } + + //-------------------------------------------------------------------------- + // LOCAL METHODS + //-------------------------------------------------------------------------- + + /** + * Clear the resource cache. + */ + public void clearCache() { + ((DroidGap)this.ctx).clearCache(); + } + + /** + * Load the url into the webview. + * + * @param url + * @param props Properties that can be passed in to the DroidGap activity (i.e. loadingDialog, wait, ...) + * @throws JSONException + */ + public void loadUrl(String url, JSONObject props) throws JSONException { + LOG.d("App", "App.loadUrl("+url+","+props+")"); + int wait = 0; + boolean openExternal = false; + boolean clearHistory = false; + + // If there are properties, then set them on the Activity + HashMap params = new HashMap(); + if (props != null) { + JSONArray keys = props.names(); + for (int i=0; i 0) { + try { + synchronized(this) { + this.wait(wait); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + ((DroidGap)this.ctx).showWebPage(url, openExternal, clearHistory, params); + } + + /** + * Cancel loadUrl before it has been loaded. + */ + public void cancelLoadUrl() { + ((DroidGap)this.ctx).cancelLoadUrl(); + } + + /** + * Clear page history for the app. + */ + public void clearHistory() { + ((DroidGap)this.ctx).clearHistory(); + } + + /** + * Go to previous page displayed. + * This is the same as pressing the backbutton on Android device. + */ + public void backHistory() { + ((DroidGap)this.ctx).backHistory(); + } + + /** + * Override the default behavior of the Android back button. + * If overridden, when the back button is pressed, the "backKeyDown" JavaScript event will be fired. + * + * @param override T=override, F=cancel override + */ + public void overrideBackbutton(boolean override) { + LOG.i("DroidGap", "WARNING: Back Button Default Behaviour will be overridden. The backbutton event will be fired!"); + ((DroidGap)this.ctx).bound = override; + } + + /** + * Return whether the Android back button is overridden by the user. + * + * @return boolean + */ + public boolean isBackbuttonOverridden() { + return ((DroidGap)this.ctx).bound; + } + + /** + * Exit the Android application. + */ + public void exitApp() { + ((DroidGap)this.ctx).endActivity(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/664a061d/framework/src/org/apache/cordova/AudioHandler.java ---------------------------------------------------------------------- diff --git a/framework/src/org/apache/cordova/AudioHandler.java b/framework/src/org/apache/cordova/AudioHandler.java new file mode 100755 index 0000000..d7df63e --- /dev/null +++ b/framework/src/org/apache/cordova/AudioHandler.java @@ -0,0 +1,364 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova; + +import android.content.Context; +import android.media.AudioManager; +import java.util.ArrayList; + +import org.apache.cordova.api.LOG; +import org.apache.cordova.api.Plugin; +import org.apache.cordova.api.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; + +import java.util.HashMap; +import java.util.Map.Entry; + +/** + * This class called by CordovaActivity to play and record audio. + * The file can be local or over a network using http. + * + * Audio formats supported (tested): + * .mp3, .wav + * + * Local audio files must reside in one of two places: + * android_asset: file name must start with /android_asset/sound.mp3 + * sdcard: file name is just sound.mp3 + */ +public class AudioHandler extends Plugin { + + public static String TAG = "AudioHandler"; + HashMap players; // Audio player object + ArrayList pausedForPhone; // Audio players that were paused when phone call came in + + /** + * Constructor. + */ + public AudioHandler() { + this.players = new HashMap(); + this.pausedForPhone = new ArrayList(); + } + + /** + * Executes the request and returns PluginResult. + * + * @param action The action to execute. + * @param args JSONArry of arguments for the plugin. + * @param callbackId The callback id used when calling back into JavaScript. + * @return A PluginResult object with a status and message. + */ + public PluginResult execute(String action, JSONArray args, String callbackId) { + PluginResult.Status status = PluginResult.Status.OK; + String result = ""; + + try { + if (action.equals("startRecordingAudio")) { + this.startRecordingAudio(args.getString(0), args.getString(1)); + } + else if (action.equals("stopRecordingAudio")) { + this.stopRecordingAudio(args.getString(0)); + } + else if (action.equals("startPlayingAudio")) { + this.startPlayingAudio(args.getString(0), args.getString(1)); + } + else if (action.equals("seekToAudio")) { + this.seekToAudio(args.getString(0), args.getInt(1)); + } + else if (action.equals("pausePlayingAudio")) { + this.pausePlayingAudio(args.getString(0)); + } + else if (action.equals("stopPlayingAudio")) { + this.stopPlayingAudio(args.getString(0)); + } else if (action.equals("setVolume")) { + try { + this.setVolume(args.getString(0), Float.parseFloat(args.getString(1))); + } catch (NumberFormatException nfe) { + //no-op + } + } else if (action.equals("getCurrentPositionAudio")) { + float f = this.getCurrentPositionAudio(args.getString(0)); + return new PluginResult(status, f); + } + else if (action.equals("getDurationAudio")) { + float f = this.getDurationAudio(args.getString(0), args.getString(1)); + return new PluginResult(status, f); + } + else if (action.equals("release")) { + boolean b = this.release(args.getString(0)); + return new PluginResult(status, b); + } + return new PluginResult(status, result); + } catch (JSONException e) { + e.printStackTrace(); + return new PluginResult(PluginResult.Status.JSON_EXCEPTION); + } + } + + /** + * Identifies if action to be executed returns a value and should be run synchronously. + * + * @param action The action to execute + * @return T=returns value + */ + public boolean isSynch(String action) { + if (action.equals("getCurrentPositionAudio")) { + return true; + } + else if (action.equals("getDurationAudio")) { + return true; + } + return false; + } + + /** + * Stop all audio players and recorders. + */ + public void onDestroy() { + for (AudioPlayer audio : this.players.values()) { + audio.destroy(); + } + this.players.clear(); + } + + /** + * Called when a message is sent to plugin. + * + * @param id The message id + * @param data The message data + */ + public void onMessage(String id, Object data) { + + // If phone message + if (id.equals("telephone")) { + + // If phone ringing, then pause playing + if ("ringing".equals(data) || "offhook".equals(data)) { + + // Get all audio players and pause them + for (AudioPlayer audio : this.players.values()) { + if (audio.getState() == AudioPlayer.MEDIA_RUNNING) { + this.pausedForPhone.add(audio); + audio.pausePlaying(); + } + } + + } + + // If phone idle, then resume playing those players we paused + else if ("idle".equals(data)) { + for (AudioPlayer audio : this.pausedForPhone) { + audio.startPlaying(null); + } + this.pausedForPhone.clear(); + } + } + } + + //-------------------------------------------------------------------------- + // LOCAL METHODS + //-------------------------------------------------------------------------- + + /** + * Release the audio player instance to save memory. + * + * @param id The id of the audio player + */ + private boolean release(String id) { + if (!this.players.containsKey(id)) { + return false; + } + AudioPlayer audio = this.players.get(id); + this.players.remove(id); + audio.destroy(); + return true; + } + + /** + * Start recording and save the specified file. + * + * @param id The id of the audio player + * @param file The name of the file + */ + public void startRecordingAudio(String id, String file) { + // If already recording, then just return; + if (this.players.containsKey(id)) { + return; + } + AudioPlayer audio = new AudioPlayer(this, id); + this.players.put(id, audio); + audio.startRecording(file); + } + + /** + * Stop recording and save to the file specified when recording started. + * + * @param id The id of the audio player + */ + public void stopRecordingAudio(String id) { + AudioPlayer audio = this.players.get(id); + if (audio != null) { + audio.stopRecording(); + this.players.remove(id); + } + } + + /** + * Start or resume playing audio file. + * + * @param id The id of the audio player + * @param file The name of the audio file. + */ + public void startPlayingAudio(String id, String file) { + AudioPlayer audio = this.players.get(id); + if (audio == null) { + audio = new AudioPlayer(this, id); + this.players.put(id, audio); + } + audio.startPlaying(file); + } + + /** + * Seek to a location. + * + * + * @param id The id of the audio player + * @param miliseconds int: number of milliseconds to skip 1000 = 1 second + */ + public void seekToAudio(String id, int milliseconds) { + AudioPlayer audio = this.players.get(id); + if (audio != null) { + audio.seekToPlaying(milliseconds); + } + } + + /** + * Pause playing. + * + * @param id The id of the audio player + */ + public void pausePlayingAudio(String id) { + AudioPlayer audio = this.players.get(id); + if (audio != null) { + audio.pausePlaying(); + } + } + + /** + * Stop playing the audio file. + * + * @param id The id of the audio player + */ + public void stopPlayingAudio(String id) { + AudioPlayer audio = this.players.get(id); + if (audio != null) { + audio.stopPlaying(); + //audio.destroy(); + //this.players.remove(id); + } + } + + /** + * Get current position of playback. + * + * @param id The id of the audio player + * @return position in msec + */ + public float getCurrentPositionAudio(String id) { + AudioPlayer audio = this.players.get(id); + if (audio != null) { + return(audio.getCurrentPosition()/1000.0f); + } + return -1; + } + + /** + * Get the duration of the audio file. + * + * @param id The id of the audio player + * @param file The name of the audio file. + * @return The duration in msec. + */ + public float getDurationAudio(String id, String file) { + + // Get audio file + AudioPlayer audio = this.players.get(id); + if (audio != null) { + return(audio.getDuration(file)); + } + + // If not already open, then open the file + else { + audio = new AudioPlayer(this, id); + this.players.put(id, audio); + return(audio.getDuration(file)); + } + } + + /** + * Set the audio device to be used for playback. + * + * @param output 1=earpiece, 2=speaker + */ + public void setAudioOutputDevice(int output) { + AudioManager audiMgr = (AudioManager) this.ctx.getSystemService(Context.AUDIO_SERVICE); + if (output == 2) { + audiMgr.setRouting(AudioManager.MODE_NORMAL, AudioManager.ROUTE_SPEAKER, AudioManager.ROUTE_ALL); + } + else if (output == 1) { + audiMgr.setRouting(AudioManager.MODE_NORMAL, AudioManager.ROUTE_EARPIECE, AudioManager.ROUTE_ALL); + } + else { + System.out.println("AudioHandler.setAudioOutputDevice() Error: Unknown output device."); + } + } + + /** + * Get the audio device to be used for playback. + * + * @return 1=earpiece, 2=speaker + */ + public int getAudioOutputDevice() { + AudioManager audiMgr = (AudioManager) this.ctx.getSystemService(Context.AUDIO_SERVICE); + if (audiMgr.getRouting(AudioManager.MODE_NORMAL) == AudioManager.ROUTE_EARPIECE) { + return 1; + } + else if (audiMgr.getRouting(AudioManager.MODE_NORMAL) == AudioManager.ROUTE_SPEAKER) { + return 2; + } + else { + return -1; + } + } + + /** + * Set the volume for an audio device + * + * @param id The id of the audio player + * @param volume Volume to adjust to 0.0f - 1.0f + */ + public void setVolume(String id, float volume) { + AudioPlayer audio = this.players.get(id); + if (audio != null) { + audio.setVolume(volume); + } else { + System.out.println("AudioHandler.setVolume() Error: Unknown Audio Player " + id); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/664a061d/framework/src/org/apache/cordova/AudioPlayer.java ---------------------------------------------------------------------- diff --git a/framework/src/org/apache/cordova/AudioPlayer.java b/framework/src/org/apache/cordova/AudioPlayer.java new file mode 100755 index 0000000..4ffeaa2 --- /dev/null +++ b/framework/src/org/apache/cordova/AudioPlayer.java @@ -0,0 +1,450 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova; + +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.media.MediaPlayer.OnPreparedListener; +import android.media.MediaRecorder; +import android.os.Environment; +import android.util.Log; + +import java.io.File; +import java.io.IOException; + +/** + * This class implements the audio playback and recording capabilities used by Cordova. + * It is called by the AudioHandler Cordova class. + * Only one file can be played or recorded per class instance. + * + * Local audio files must reside in one of two places: + * android_asset: file name must start with /android_asset/sound.mp3 + * sdcard: file name is just sound.mp3 + */ +public class AudioPlayer implements OnCompletionListener, OnPreparedListener, OnErrorListener { + + private static final String LOG_TAG = "AudioPlayer"; + + // AudioPlayer states + public static int MEDIA_NONE = 0; + public static int MEDIA_STARTING = 1; + public static int MEDIA_RUNNING = 2; + public static int MEDIA_PAUSED = 3; + public static int MEDIA_STOPPED = 4; + + // AudioPlayer message ids + private static int MEDIA_STATE = 1; + private static int MEDIA_DURATION = 2; + private static int MEDIA_POSITION = 3; + private static int MEDIA_ERROR = 9; + + // Media error codes + private static int MEDIA_ERR_NONE_ACTIVE = 0; + private static int MEDIA_ERR_ABORTED = 1; + private static int MEDIA_ERR_NETWORK = 2; + private static int MEDIA_ERR_DECODE = 3; + private static int MEDIA_ERR_NONE_SUPPORTED = 4; + + private AudioHandler handler; // The AudioHandler object + private String id; // The id of this player (used to identify Media object in JavaScript) + private int state = MEDIA_NONE; // State of recording or playback + private String audioFile = null; // File name to play or record to + private float duration = -1; // Duration of audio + + private MediaRecorder recorder = null; // Audio recording object + private String tempFile = null; // Temporary recording file name + + private MediaPlayer mPlayer = null; // Audio player object + private boolean prepareOnly = false; + + /** + * Constructor. + * + * @param handler The audio handler object + * @param id The id of this audio player + */ + public AudioPlayer(AudioHandler handler, String id) { + this.handler = handler; + this.id = id; + this.tempFile = Environment.getExternalStorageDirectory().getAbsolutePath() + "/tmprecording.mp3"; + } + + /** + * Destroy player and stop audio playing or recording. + */ + public void destroy() { + + // Stop any play or record + if (this.mPlayer != null) { + if ((this.state == MEDIA_RUNNING) || (this.state == MEDIA_PAUSED)) { + this.mPlayer.stop(); + this.setState(MEDIA_STOPPED); + } + this.mPlayer.release(); + this.mPlayer = null; + } + if (this.recorder != null) { + this.stopRecording(); + this.recorder.release(); + this.recorder = null; + } + } + + /** + * Start recording the specified file. + * + * @param file The name of the file + */ + public void startRecording(String file) { + if (this.mPlayer != null) { + Log.d(LOG_TAG, "AudioPlayer Error: Can't record in play mode."); + this.handler.sendJavascript("Cordova.Media.onStatus('" + this.id + "', "+MEDIA_ERROR+", "+MEDIA_ERR_ABORTED+");"); + } + + // Make sure we're not already recording + else if (this.recorder == null) { + this.audioFile = file; + this.recorder = new MediaRecorder(); + this.recorder.setAudioSource(MediaRecorder.AudioSource.MIC); + this.recorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); // THREE_GPP); + this.recorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT); //AMR_NB); + this.recorder.setOutputFile(this.tempFile); + try { + this.recorder.prepare(); + this.recorder.start(); + this.setState(MEDIA_RUNNING); + return; + } catch (IllegalStateException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + this.handler.sendJavascript("Cordova.Media.onStatus('" + this.id + "', "+MEDIA_ERROR+", "+MEDIA_ERR_ABORTED+");"); + } + else { + Log.d(LOG_TAG, "AudioPlayer Error: Already recording."); + this.handler.sendJavascript("Cordova.Media.onStatus('" + this.id + "', "+MEDIA_ERROR+", "+MEDIA_ERR_ABORTED+");"); + } + } + + /** + * Save temporary recorded file to specified name + * + * @param file + */ + public void moveFile(String file) { + + /* this is a hack to save the file as the specified name */ + File f = new File(this.tempFile); + f.renameTo(new File("/sdcard/" + file)); + } + + /** + * Stop recording and save to the file specified when recording started. + */ + public void stopRecording() { + if (this.recorder != null) { + try{ + if (this.state == MEDIA_RUNNING) { + this.recorder.stop(); + this.setState(MEDIA_STOPPED); + } + this.moveFile(this.audioFile); + } + catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * Start or resume playing audio file. + * + * @param file The name of the audio file. + */ + public void startPlaying(String file) { + if (this.recorder != null) { + Log.d(LOG_TAG, "AudioPlayer Error: Can't play in record mode."); + this.handler.sendJavascript("Cordova.Media.onStatus('" + this.id + "', "+MEDIA_ERROR+", "+MEDIA_ERR_ABORTED+");"); + } + + // If this is a new request to play audio, or stopped + else if ((this.mPlayer == null) || (this.state == MEDIA_STOPPED)) { + try { + // If stopped, then reset player + if (this.mPlayer != null) { + this.mPlayer.reset(); + } + // Otherwise, create a new one + else { + this.mPlayer = new MediaPlayer(); + } + this.audioFile = file; + + // If streaming file + if (this.isStreaming(file)) { + this.mPlayer.setDataSource(file); + this.mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + this.setState(MEDIA_STARTING); + this.mPlayer.setOnPreparedListener(this); + this.mPlayer.prepareAsync(); + } + + // If local file + else { + if (file.startsWith("/android_asset/")) { + String f = file.substring(15); + android.content.res.AssetFileDescriptor fd = this.handler.ctx.getBaseContext().getAssets().openFd(f); + this.mPlayer.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength()); + } + else { + this.mPlayer.setDataSource("/sdcard/" + file); + } + this.setState(MEDIA_STARTING); + this.mPlayer.setOnPreparedListener(this); + this.mPlayer.prepare(); + + // Get duration + this.duration = getDurationInSeconds(); + } + } + catch (Exception e) { + e.printStackTrace(); + this.handler.sendJavascript("Cordova.Media.onStatus('" + this.id + "', "+MEDIA_ERROR+", "+MEDIA_ERR_ABORTED+");"); + } + } + + // If we have already have created an audio player + else { + + // If player has been paused, then resume playback + if ((this.state == MEDIA_PAUSED) || (this.state == MEDIA_STARTING)) { + this.mPlayer.start(); + this.setState(MEDIA_RUNNING); + } + else { + Log.d(LOG_TAG, "AudioPlayer Error: startPlaying() called during invalid state: "+this.state); + this.handler.sendJavascript("Cordova.Media.onStatus('" + this.id + "', "+MEDIA_ERROR+", "+MEDIA_ERR_ABORTED+");"); + } + } + } + + /** + * Seek or jump to a new time in the track. + */ + public void seekToPlaying(int milliseconds) { + if (this.mPlayer != null) { + this.mPlayer.seekTo(milliseconds); + Log.d(LOG_TAG, "Send a onStatus update for the new seek"); + this.handler.sendJavascript("Cordova.Media.onStatus('" + this.id + "', "+MEDIA_POSITION+", "+milliseconds/1000.0f+");"); + } + } + + /** + * Pause playing. + */ + public void pausePlaying() { + + // If playing, then pause + if (this.state == MEDIA_RUNNING) { + this.mPlayer.pause(); + this.setState(MEDIA_PAUSED); + } + else { + Log.d(LOG_TAG, "AudioPlayer Error: pausePlaying() called during invalid state: "+this.state); + this.handler.sendJavascript("Cordova.Media.onStatus('" + this.id + "', "+MEDIA_ERROR+", "+MEDIA_ERR_NONE_ACTIVE+");"); + } + } + + /** + * Stop playing the audio file. + */ + public void stopPlaying() { + if ((this.state == MEDIA_RUNNING) || (this.state == MEDIA_PAUSED)) { + this.mPlayer.stop(); + this.setState(MEDIA_STOPPED); + } + else { + Log.d(LOG_TAG, "AudioPlayer Error: stopPlaying() called during invalid state: "+this.state); + this.handler.sendJavascript("Cordova.Media.onStatus('" + this.id + "', "+MEDIA_ERROR+", "+MEDIA_ERR_NONE_ACTIVE+");"); + } + } + + /** + * Callback to be invoked when playback of a media source has completed. + * + * @param mPlayer The MediaPlayer that reached the end of the file + */ + public void onCompletion(MediaPlayer mPlayer) { + this.setState(MEDIA_STOPPED); + } + + /** + * Get current position of playback. + * + * @return position in msec or -1 if not playing + */ + public long getCurrentPosition() { + if ((this.state == MEDIA_RUNNING) || (this.state == MEDIA_PAUSED)) { + int curPos = this.mPlayer.getCurrentPosition(); + this.handler.sendJavascript("Cordova.Media.onStatus('" + this.id + "', "+MEDIA_POSITION+", "+curPos/1000.0f+");"); + return curPos; + } + else { + return -1; + } + } + + /** + * Determine if playback file is streaming or local. + * It is streaming if file name starts with "http://" + * + * @param file The file name + * @return T=streaming, F=local + */ + public boolean isStreaming(String file) { + if (file.contains("http://") || file.contains("https://")) { + return true; + } + else { + return false; + } + } + + /** + * Get the duration of the audio file. + * + * @param file The name of the audio file. + * @return The duration in msec. + * -1=can't be determined + * -2=not allowed + */ + public float getDuration(String file) { + + // Can't get duration of recording + if (this.recorder != null) { + return(-2); // not allowed + } + + // If audio file already loaded and started, then return duration + if (this.mPlayer != null) { + return this.duration; + } + + // If no player yet, then create one + else { + this.prepareOnly = true; + this.startPlaying(file); + + // This will only return value for local, since streaming + // file hasn't been read yet. + return this.duration; + } + } + + /** + * Callback to be invoked when the media source is ready for playback. + * + * @param mPlayer The MediaPlayer that is ready for playback + */ + public void onPrepared(MediaPlayer mPlayer) { + // Listen for playback completion + this.mPlayer.setOnCompletionListener(this); + + // If start playing after prepared + if (!this.prepareOnly) { + + // Start playing + this.mPlayer.start(); + + // Set player init flag + this.setState(MEDIA_RUNNING); + } + + // Save off duration + this.duration = getDurationInSeconds(); + this.prepareOnly = false; + + // Send status notification to JavaScript + this.handler.sendJavascript("Cordova.Media.onStatus('" + this.id + "', "+MEDIA_DURATION+","+this.duration+");"); + + } + + /** + * By default Android returns the length of audio in mills but we want seconds + * + * @return length of clip in seconds + */ + private float getDurationInSeconds() { + return (this.mPlayer.getDuration() / 1000.0f); + } + + /** + * Callback to be invoked when there has been an error during an asynchronous operation + * (other errors will throw exceptions at method call time). + * + * @param mPlayer the MediaPlayer the error pertains to + * @param arg1 the type of error that has occurred: (MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_SERVER_DIED) + * @param arg2 an extra code, specific to the error. + */ + public boolean onError(MediaPlayer mPlayer, int arg1, int arg2) { + Log.d(LOG_TAG, "AudioPlayer.onError(" + arg1 + ", " + arg2+")"); + + // TODO: Not sure if this needs to be sent? + this.mPlayer.stop(); + this.mPlayer.release(); + + // Send error notification to JavaScript + this.handler.sendJavascript("Cordova.Media.onStatus('" + this.id + "', "+MEDIA_ERROR+", "+arg1+");"); + return false; + } + + /** + * Set the state and send it to JavaScript. + * + * @param state + */ + private void setState(int state) { + if (this.state != state) { + this.handler.sendJavascript("Cordova.Media.onStatus('" + this.id + "', "+MEDIA_STATE+", "+state+");"); + } + + this.state = state; + } + + /** + * Get the audio state. + * + * @return int + */ + public int getState() { + return this.state; + } + + /** + * Set the volume for audio player + * + * @param volume + */ + public void setVolume(float volume) { + this.mPlayer.setVolume(volume, volume); + } +} http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/664a061d/framework/src/org/apache/cordova/AuthenticationToken.java ---------------------------------------------------------------------- diff --git a/framework/src/org/apache/cordova/AuthenticationToken.java b/framework/src/org/apache/cordova/AuthenticationToken.java new file mode 100644 index 0000000..9ed6f2d --- /dev/null +++ b/framework/src/org/apache/cordova/AuthenticationToken.java @@ -0,0 +1,51 @@ +package org.apache.cordova; + +/** + * The Class AuthenticationToken defines the userName and password to be used for authenticating a web resource + */ +public class AuthenticationToken { + private String userName; + private String password; + + /** + * Gets the user name. + * + * @return the user name + */ + public String getUserName() { + return userName; + } + + /** + * Sets the user name. + * + * @param userName + * the new user name + */ + public void setUserName(String userName) { + this.userName = userName; + } + + /** + * Gets the password. + * + * @return the password + */ + public String getPassword() { + return password; + } + + /** + * Sets the password. + * + * @param password + * the new password + */ + public void setPassword(String password) { + this.password = password; + } + + + + +} http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/664a061d/framework/src/org/apache/cordova/BatteryListener.java ---------------------------------------------------------------------- diff --git a/framework/src/org/apache/cordova/BatteryListener.java b/framework/src/org/apache/cordova/BatteryListener.java new file mode 100755 index 0000000..84024b2 --- /dev/null +++ b/framework/src/org/apache/cordova/BatteryListener.java @@ -0,0 +1,156 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova; + +import org.apache.cordova.api.Plugin; +import org.apache.cordova.api.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; + +public class BatteryListener extends Plugin { + + private static final String LOG_TAG = "BatteryManager"; + + BroadcastReceiver receiver; + + private String batteryCallbackId = null; + + /** + * Constructor. + */ + public BatteryListener() { + this.receiver = null; + } + + /** + * Executes the request and returns PluginResult. + * + * @param action The action to execute. + * @param args JSONArry of arguments for the plugin. + * @param callbackId The callback id used when calling back into JavaScript. + * @return A PluginResult object with a status and message. + */ + public PluginResult execute(String action, JSONArray args, String callbackId) { + PluginResult.Status status = PluginResult.Status.INVALID_ACTION; + String result = "Unsupported Operation: " + action; + + if (action.equals("start")) { + if (this.batteryCallbackId != null) { + return new PluginResult(PluginResult.Status.ERROR, "Battery listener already running."); + } + this.batteryCallbackId = callbackId; + + // We need to listen to power events to update battery status + IntentFilter intentFilter = new IntentFilter() ; + intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED); + if (this.receiver == null) { + this.receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateBatteryInfo(intent); + } + }; + ctx.registerReceiver(this.receiver, intentFilter); + } + + // Don't return any result now, since status results will be sent when events come in from broadcast receiver + PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT); + pluginResult.setKeepCallback(true); + return pluginResult; + } + + else if (action.equals("stop")) { + removeBatteryListener(); + this.sendUpdate(new JSONObject(), false); // release status callback in JS side + this.batteryCallbackId = null; + return new PluginResult(PluginResult.Status.OK); + } + + return new PluginResult(status, result); + } + + /** + * Stop battery receiver. + */ + public void onDestroy() { + removeBatteryListener(); + } + + /** + * Stop the battery receiver and set it to null. + */ + private void removeBatteryListener() { + if (this.receiver != null) { + try { + this.ctx.unregisterReceiver(this.receiver); + this.receiver = null; + } catch (Exception e) { + Log.e(LOG_TAG, "Error unregistering battery receiver: " + e.getMessage(), e); + } + } + } + + /** + * Creates a JSONObject with the current battery information + * + * @param batteryIntent the current battery information + * @return a JSONObject containing the battery status information + */ + private JSONObject getBatteryInfo(Intent batteryIntent) { + JSONObject obj = new JSONObject(); + try { + obj.put("level", batteryIntent.getIntExtra(android.os.BatteryManager.EXTRA_LEVEL, 0)); + obj.put("isPlugged", batteryIntent.getIntExtra(android.os.BatteryManager.EXTRA_PLUGGED, -1) > 0 ? true : false); + } catch (JSONException e) { + Log.e(LOG_TAG, e.getMessage(), e); + } + return obj; + } + + /** + * Updates the JavaScript side whenever the battery changes + * + * @param batteryIntent the current battery information + * @return + */ + private void updateBatteryInfo(Intent batteryIntent) { + sendUpdate(this.getBatteryInfo(batteryIntent), true); + } + + /** + * Create a new plugin result and send it back to JavaScript + * + * @param connection the network info to set as navigator.connection + */ + private void sendUpdate(JSONObject info, boolean keepCallback) { + if (this.batteryCallbackId != null) { + PluginResult result = new PluginResult(PluginResult.Status.OK, info); + result.setKeepCallback(keepCallback); + this.success(result, this.batteryCallbackId); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/664a061d/framework/src/org/apache/cordova/CallbackServer.java ---------------------------------------------------------------------- diff --git a/framework/src/org/apache/cordova/CallbackServer.java b/framework/src/org/apache/cordova/CallbackServer.java new file mode 100755 index 0000000..6b6ce70 --- /dev/null +++ b/framework/src/org/apache/cordova/CallbackServer.java @@ -0,0 +1,431 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URLEncoder; +import java.util.LinkedList; + +import android.util.Log; + +/** + * This class provides a way for Java to run JavaScript in the web page that has loaded Cordova. + * The CallbackServer class implements an XHR server and a polling server with a list of JavaScript + * statements that are to be executed on the web page. + * + * The process flow for XHR is: + * 1. JavaScript makes an async XHR call. + * 2. The server holds the connection open until data is available. + * 3. The server writes the data to the client and closes the connection. + * 4. The server immediately starts listening for the next XHR call. + * 5. The client receives this XHR response, processes it. + * 6. The client sends a new async XHR request. + * + * The CallbackServer class requires the following permission in Android manifest file + * + * + * If the device has a proxy set, then XHR cannot be used, so polling must be used instead. + * This can be determined by the client by calling CallbackServer.usePolling(). + * + * The process flow for polling is: + * 1. The client calls CallbackServer.getJavascript() to retrieve next statement. + * 2. If statement available, then client processes it. + * 3. The client repeats #1 in loop. + */ +public class CallbackServer implements Runnable { + + private static final String LOG_TAG = "CallbackServer"; + + /** + * The list of JavaScript statements to be sent to JavaScript. + */ + private LinkedList javascript; + + /** + * The port to listen on. + */ + private int port; + + /** + * The server thread. + */ + private Thread serverThread; + + /** + * Indicates the server is running. + */ + private boolean active; + + /** + * Indicates that the JavaScript statements list is empty + */ + private boolean empty; + + /** + * Indicates that polling should be used instead of XHR. + */ + private boolean usePolling = true; + + /** + * Security token to prevent other apps from accessing this callback server via XHR + */ + private String token; + + /** + * Constructor. + */ + public CallbackServer() { + //System.out.println("CallbackServer()"); + this.active = false; + this.empty = true; + this.port = 0; + this.javascript = new LinkedList(); + } + + /** + * Init callback server and start XHR if running local app. + * + * If Cordova app is loaded from file://, then we can use XHR + * otherwise we have to use polling due to cross-domain security restrictions. + * + * @param url The URL of the Cordova app being loaded + */ + public void init(String url) { + //System.out.println("CallbackServer.start("+url+")"); + this.active = false; + this.empty = true; + this.port = 0; + this.javascript = new LinkedList(); + + // Determine if XHR or polling is to be used + if ((url != null) && !url.startsWith("file://")) { + this.usePolling = true; + this.stopServer(); + } + else if (android.net.Proxy.getDefaultHost() != null) { + this.usePolling = true; + this.stopServer(); + } + else { + this.usePolling = false; + this.startServer(); + } + } + + /** + * Re-init when loading a new HTML page into webview. + * + * @param url The URL of the Cordova app being loaded + */ + public void reinit(String url) { + this.stopServer(); + this.init(url); + } + + /** + * Return if polling is being used instead of XHR. + * + * @return + */ + public boolean usePolling() { + return this.usePolling; + } + + /** + * Get the port that this server is running on. + * + * @return + */ + public int getPort() { + return this.port; + } + + /** + * Get the security token that this server requires when calling getJavascript(). + * + * @return + */ + public String getToken() { + return this.token; + } + + /** + * Start the server on a new thread. + */ + public void startServer() { + //System.out.println("CallbackServer.startServer()"); + this.active = false; + + // Start server on new thread + this.serverThread = new Thread(this); + this.serverThread.start(); + } + + /** + * Restart the server on a new thread. + */ + public void restartServer() { + + // Stop server + this.stopServer(); + + // Start server again + this.startServer(); + } + + /** + * Start running the server. + * This is called automatically when the server thread is started. + */ + public void run() { + + // Start server + try { + this.active = true; + String request; + ServerSocket waitSocket = new ServerSocket(0); + this.port = waitSocket.getLocalPort(); + //System.out.println("CallbackServer -- using port " +this.port); + this.token = java.util.UUID.randomUUID().toString(); + //System.out.println("CallbackServer -- using token "+this.token); + + while (this.active) { + //System.out.println("CallbackServer: Waiting for data on socket"); + Socket connection = waitSocket.accept(); + BufferedReader xhrReader = new BufferedReader(new InputStreamReader(connection.getInputStream()),40); + DataOutputStream output = new DataOutputStream(connection.getOutputStream()); + request = xhrReader.readLine(); + String response = ""; + //System.out.println("CallbackServerRequest="+request); + if (this.active && (request != null)) { + if (request.contains("GET")) { + + // Get requested file + String[] requestParts = request.split(" "); + + // Must have security token + if ((requestParts.length == 3) && (requestParts[1].substring(1).equals(this.token))) { + //System.out.println("CallbackServer -- Processing GET request"); + + // Wait until there is some data to send, or send empty data every 10 sec + // to prevent XHR timeout on the client + synchronized (this) { + while (this.empty) { + try { + this.wait(10000); // prevent timeout from happening + //System.out.println("CallbackServer>>> break <<<"); + break; + } + catch (Exception e) { } + } + } + + // If server is still running + if (this.active) { + + // If no data, then send 404 back to client before it times out + if (this.empty) { + //System.out.println("CallbackServer -- sending data 0"); + response = "HTTP/1.1 404 NO DATA\r\n\r\n "; // need to send content otherwise some Android devices fail, so send space + } + else { + //System.out.println("CallbackServer -- sending item"); + response = "HTTP/1.1 200 OK\r\n\r\n"; + String js = this.getJavascript(); + if (js != null) { + response += encode(js, "UTF-8"); + } + } + } + else { + response = "HTTP/1.1 503 Service Unavailable\r\n\r\n "; + } + } + else { + response = "HTTP/1.1 403 Forbidden\r\n\r\n "; + } + } + else { + response = "HTTP/1.1 400 Bad Request\r\n\r\n "; + } + //System.out.println("CallbackServer: response="+response); + //System.out.println("CallbackServer: closing output"); + output.writeBytes(response); + output.flush(); + } + output.close(); + xhrReader.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + this.active = false; + //System.out.println("CallbackServer.startServer() - EXIT"); + } + + /** + * Stop server. + * This stops the thread that the server is running on. + */ + public void stopServer() { + //System.out.println("CallbackServer.stopServer()"); + if (this.active) { + this.active = false; + + // Break out of server wait + synchronized (this) { + this.notify(); + } + } + } + + /** + * Destroy + */ + public void destroy() { + this.stopServer(); + } + + /** + * Get the number of JavaScript statements. + * + * @return int + */ + public int getSize() { + synchronized(this) { + int size = this.javascript.size(); + return size; + } + } + + /** + * Get the next JavaScript statement and remove from list. + * + * @return String + */ + public String getJavascript() { + synchronized(this) { + if (this.javascript.size() == 0) { + return null; + } + String statement = this.javascript.remove(0); + if (this.javascript.size() == 0) { + this.empty = true; + } + return statement; + } + } + + /** + * Add a JavaScript statement to the list. + * + * @param statement + */ + public void sendJavascript(String statement) { + synchronized (this) { + this.javascript.add(statement); + this.empty = false; + this.notify(); + } + } + + /* The Following code has been modified from original implementation of URLEncoder */ + + /* start */ + + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + static final String digits = "0123456789ABCDEF"; + + /** + * This will encode the return value to JavaScript. We revert the encoding for + * common characters that don't require encoding to reduce the size of the string + * being passed to JavaScript. + * + * @param s to be encoded + * @param enc encoding type + * @return encoded string + */ + public static String encode(String s, String enc) throws UnsupportedEncodingException { + if (s == null || enc == null) { + throw new NullPointerException(); + } + // check for UnsupportedEncodingException + "".getBytes(enc); + + // Guess a bit bigger for encoded form + StringBuilder buf = new StringBuilder(s.length() + 16); + int start = -1; + 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) { + if (start >= 0) { + convert(s.substring(start, i), buf, enc); + start = -1; + } + if (ch != ' ') { + buf.append(ch); + } else { + buf.append(' '); + } + } else { + if (start < 0) { + start = i; + } + } + } + if (start >= 0) { + convert(s.substring(start, s.length()), buf, enc); + } + return buf.toString(); + } + + private static void convert(String s, StringBuilder buf, String enc) throws UnsupportedEncodingException { + byte[] bytes = s.getBytes(enc); + for (int j = 0; j < bytes.length; j++) { + buf.append('%'); + buf.append(digits.charAt((bytes[j] & 0xf0) >> 4)); + buf.append(digits.charAt(bytes[j] & 0xf)); + } + } + + /* end */ +}