diff --git a/build/sdk.atree b/build/sdk.atree index cb15d5a95..35d34dd6d 100644 --- a/build/sdk.atree +++ b/build/sdk.atree @@ -160,6 +160,7 @@ development/samples/MultiResolution samples/${PLATFORM_NAME}/MultiResol development/samples/NotePad samples/${PLATFORM_NAME}/NotePad development/samples/SampleSyncAdapter samples/${PLATFORM_NAME}/SampleSyncAdapter development/samples/SearchableDictionary samples/${PLATFORM_NAME}/SearchableDictionary +development/samples/SipDemo samples/${PLATFORM_NAME}/SipDemo development/samples/SkeletonApp samples/${PLATFORM_NAME}/SkeletonApp development/samples/Snake samples/${PLATFORM_NAME}/Snake development/samples/SoftKeyboard samples/${PLATFORM_NAME}/SoftKeyboard diff --git a/samples/SipDemo/Android.mk b/samples/SipDemo/Android.mk new file mode 100644 index 000000000..6363727ce --- /dev/null +++ b/samples/SipDemo/Android.mk @@ -0,0 +1,16 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := samples + +# Only compile source java files in this apk. +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_PACKAGE_NAME := SipDemo + +LOCAL_SDK_VERSION := current + +include $(BUILD_PACKAGE) + +# Use the following include to make our test apk. +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/samples/SipDemo/AndroidManifest.xml b/samples/SipDemo/AndroidManifest.xml new file mode 100644 index 000000000..8fb767569 --- /dev/null +++ b/samples/SipDemo/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/SipDemo/_index.html b/samples/SipDemo/_index.html new file mode 100644 index 000000000..dbc90373f --- /dev/null +++ b/samples/SipDemo/_index.html @@ -0,0 +1,52 @@ +

This is a demo application highlighting how to make internet-based calls with +the SIP API. The application uses a walkie-talkie style interface, allowing you +to only be heard when the button is pushed down.

+ +

The source code for this demo app shows how to accomplish three key things +with SIP: Make a call, receive a call, and signal to the Android platform that +your app wants to receive incoming SIP calls, so that they can be handled from +within the application.

+ + +

The application includes:

+

+

If you are developing an application that uses the SIP API, remember that the +feature is supported only on Android 2.3 (API level 9) and higher versions of +the platform. Also, among devices running Android 2.3 (API level 9) or higher, +not all devices will offer SIP support. To ensure that your application can only +be installed on devices that are capable of supporting SIP, remember to add the +following to the application's manifest before publishing to Android Market:

+

To control how Android Market filters your application +from devices that do not support SIP, remember to add the following to the +application's manifest

+

For more information about using the SIP API, see the android.net.sip +documentation.

+ + diff --git a/samples/SipDemo/res/drawable/btn_record.xml b/samples/SipDemo/res/drawable/btn_record.xml new file mode 100644 index 000000000..a803da11c --- /dev/null +++ b/samples/SipDemo/res/drawable/btn_record.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/samples/SipDemo/res/drawable/btn_speak_normal.png b/samples/SipDemo/res/drawable/btn_speak_normal.png new file mode 100644 index 000000000..fb86ceba1 Binary files /dev/null and b/samples/SipDemo/res/drawable/btn_speak_normal.png differ diff --git a/samples/SipDemo/res/drawable/btn_speak_pressed.png b/samples/SipDemo/res/drawable/btn_speak_pressed.png new file mode 100644 index 000000000..cffdf918c Binary files /dev/null and b/samples/SipDemo/res/drawable/btn_speak_pressed.png differ diff --git a/samples/SipDemo/res/drawable/btn_speak_selected.png b/samples/SipDemo/res/drawable/btn_speak_selected.png new file mode 100644 index 000000000..99a1a6f57 Binary files /dev/null and b/samples/SipDemo/res/drawable/btn_speak_selected.png differ diff --git a/samples/SipDemo/res/drawable/icon.png b/samples/SipDemo/res/drawable/icon.png new file mode 100644 index 000000000..64e3601c2 Binary files /dev/null and b/samples/SipDemo/res/drawable/icon.png differ diff --git a/samples/SipDemo/res/layout/call_address_dialog.xml b/samples/SipDemo/res/layout/call_address_dialog.xml new file mode 100644 index 000000000..2bd75565e --- /dev/null +++ b/samples/SipDemo/res/layout/call_address_dialog.xml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/samples/SipDemo/res/layout/walkietalkie.xml b/samples/SipDemo/res/layout/walkietalkie.xml new file mode 100644 index 000000000..bdf634c84 --- /dev/null +++ b/samples/SipDemo/res/layout/walkietalkie.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + diff --git a/samples/SipDemo/res/values/strings.xml b/samples/SipDemo/res/values/strings.xml new file mode 100644 index 000000000..a0d49bc0a --- /dev/null +++ b/samples/SipDemo/res/values/strings.xml @@ -0,0 +1,22 @@ + + + + + SIP Demo + SIP Address to contact + Talk + diff --git a/samples/SipDemo/res/xml/preferences.xml b/samples/SipDemo/res/xml/preferences.xml new file mode 100644 index 000000000..590e0924c --- /dev/null +++ b/samples/SipDemo/res/xml/preferences.xml @@ -0,0 +1,38 @@ + + + + + + + + diff --git a/samples/SipDemo/src/com/example/android/sip/IncomingCallReceiver.java b/samples/SipDemo/src/com/example/android/sip/IncomingCallReceiver.java new file mode 100644 index 000000000..ea27a0a85 --- /dev/null +++ b/samples/SipDemo/src/com/example/android/sip/IncomingCallReceiver.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.sip; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.sip.*; +import android.util.Log; + +/** + * Listens for incoming SIP calls, intercepts and hands them off to WalkieTalkieActivity. + */ +public class IncomingCallReceiver extends BroadcastReceiver { + /** + * Processes the incoming call, answers it, and hands it over to the + * WalkieTalkieActivity. + * @param context The context under which the receiver is running. + * @param intent The intent being received. + */ + @Override + public void onReceive(Context context, Intent intent) { + SipAudioCall incomingCall = null; + try { + + SipAudioCall.Listener listener = new SipAudioCall.Listener() { + @Override + public void onRinging(SipAudioCall call, SipProfile caller) { + try { + call.answerCall(30); + } catch (Exception e) { + e.printStackTrace(); + } + } + }; + + WalkieTalkieActivity wtActivity = (WalkieTalkieActivity) context; + + incomingCall = wtActivity.manager.takeAudioCall(intent, listener); + incomingCall.answerCall(30); + incomingCall.startAudio(); + incomingCall.setSpeakerMode(true); + if(incomingCall.isMuted()) { + incomingCall.toggleMute(); + } + + wtActivity.call = incomingCall; + + wtActivity.updateStatus(incomingCall); + + } catch (Exception e) { + if (incomingCall != null) { + incomingCall.close(); + } + } + } + +} diff --git a/samples/SipDemo/src/com/example/android/sip/SipSettings.java b/samples/SipDemo/src/com/example/android/sip/SipSettings.java new file mode 100644 index 000000000..2f0624bbf --- /dev/null +++ b/samples/SipDemo/src/com/example/android/sip/SipSettings.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.sip; + +import android.os.Bundle; +import android.preference.PreferenceActivity; + +/** + * Handles SIP authentication settings for the Walkie Talkie app. + */ +public class SipSettings extends PreferenceActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + // Note that none of the preferences are actually defined here. + // They're all in the XML file res/xml/preferences.xml. + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.preferences); + } +} diff --git a/samples/SipDemo/src/com/example/android/sip/WalkieTalkieActivity.java b/samples/SipDemo/src/com/example/android/sip/WalkieTalkieActivity.java new file mode 100644 index 000000000..4c187eddc --- /dev/null +++ b/samples/SipDemo/src/com/example/android/sip/WalkieTalkieActivity.java @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.sip; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.PendingIntent; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.*; +import android.net.sip.*; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.ToggleButton; + +import java.text.ParseException; + +/** + * Handles all calling, receiving calls, and UI interaction in the WalkieTalkie app. + */ +public class WalkieTalkieActivity extends Activity implements View.OnTouchListener { + + public String sipAddress = null; + + public SipManager manager = null; + public SipProfile me = null; + public SipAudioCall call = null; + public IncomingCallReceiver callReceiver; + + private static final int CALL_ADDRESS = 1; + private static final int SET_AUTH_INFO = 2; + private static final int UPDATE_SETTINGS_DIALOG = 3; + private static final int HANG_UP = 4; + + + @Override + public void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + setContentView(R.layout.walkietalkie); + + ToggleButton pushToTalkButton = (ToggleButton) findViewById(R.id.pushToTalk); + pushToTalkButton.setOnTouchListener(this); + + // Set up the intent filter. This will be used to fire an + // IncomingCallReceiver when someone calls the SIP address used by this + // application. + IntentFilter filter = new IntentFilter(); + filter.addAction("android.SipDemo.INCOMING_CALL"); + callReceiver = new IncomingCallReceiver(); + this.registerReceiver(callReceiver, filter); + + // "Push to talk" can be a serious pain when the screen keeps turning off. + // Let's prevent that. + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + initializeManager(); + } + + @Override + public void onStart() { + super.onStart(); + // When we get back from the preference setting Activity, assume + // settings have changed, and re-login with new auth info. + initializeManager(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (call != null) { + call.close(); + } + + closeLocalProfile(); + + if (callReceiver != null) { + this.unregisterReceiver(callReceiver); + } + } + + public void initializeManager() { + if(manager == null) { + manager = SipManager.newInstance(this); + } + + initializeLocalProfile(); + } + + /** + * Logs you into your SIP provider, registering this device as the location to + * send SIP calls to for your SIP address. + */ + public void initializeLocalProfile() { + if (manager == null) { + return; + } + + if (me != null) { + closeLocalProfile(); + } + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); + String username = prefs.getString("namePref", ""); + String domain = prefs.getString("domainPref", ""); + String password = prefs.getString("passPref", ""); + + if (username.length() == 0 || domain.length() == 0 || password.length() == 0) { + showDialog(UPDATE_SETTINGS_DIALOG); + return; + } + + try { + SipProfile.Builder builder = new SipProfile.Builder(username, domain); + builder.setPassword(password); + me = builder.build(); + + Intent i = new Intent(); + i.setAction("android.SipDemo.INCOMING_CALL"); + PendingIntent pi = PendingIntent.getBroadcast(this, 0, i, Intent.FILL_IN_DATA); + manager.open(me, pi, null); + + + // This listener must be added AFTER manager.open is called, + // Otherwise the methods aren't guaranteed to fire. + + manager.setRegistrationListener(me.getUriString(), new SipRegistrationListener() { + public void onRegistering(String localProfileUri) { + updateStatus("Registering with SIP Server..."); + } + + public void onRegistrationDone(String localProfileUri, long expiryTime) { + updateStatus("Ready"); + } + + public void onRegistrationFailed(String localProfileUri, int errorCode, + String errorMessage) { + updateStatus("Registration failed. Please check settings."); + } + }); + } catch (ParseException pe) { + updateStatus("Connection Error."); + } catch (SipException se) { + updateStatus("Connection error."); + } + } + + /** + * Closes out your local profile, freeing associated objects into memory + * and unregistering your device from the server. + */ + public void closeLocalProfile() { + if (manager == null) { + return; + } + try { + if (me != null) { + manager.close(me.getUriString()); + } + } catch (Exception ee) { + Log.d("WalkieTalkieActivity/onDestroy", "Failed to close local profile.", ee); + } + } + + /** + * Make an outgoing call. + */ + public void initiateCall() { + + updateStatus(sipAddress); + + try { + SipAudioCall.Listener listener = new SipAudioCall.Listener() { + // Much of the client's interaction with the SIP Stack will + // happen via listeners. Even making an outgoing call, don't + // forget to set up a listener to set things up once the call is established. + @Override + public void onCallEstablished(SipAudioCall call) { + call.startAudio(); + call.setSpeakerMode(true); + call.toggleMute(); + updateStatus(call); + } + + @Override + public void onCallEnded(SipAudioCall call) { + updateStatus("Ready."); + } + }; + + call = manager.makeAudioCall(me.getUriString(), sipAddress, listener, 30); + + } + catch (Exception e) { + Log.i("WalkieTalkieActivity/InitiateCall", "Error when trying to close manager.", e); + if (me != null) { + try { + manager.close(me.getUriString()); + } catch (Exception ee) { + Log.i("WalkieTalkieActivity/InitiateCall", + "Error when trying to close manager.", ee); + ee.printStackTrace(); + } + } + if (call != null) { + call.close(); + } + } + } + + /** + * Updates the status box at the top of the UI with a messege of your choice. + * @param status The String to display in the status box. + */ + public void updateStatus(final String status) { + // Be a good citizen. Make sure UI changes fire on the UI thread. + this.runOnUiThread(new Runnable() { + public void run() { + TextView labelView = (TextView) findViewById(R.id.sipLabel); + labelView.setText(status); + } + }); + } + + /** + * Updates the status box with the SIP address of the current call. + * @param call The current, active call. + */ + public void updateStatus(SipAudioCall call) { + String useName = call.getPeerProfile().getDisplayName(); + if(useName == null) { + useName = call.getPeerProfile().getUserName(); + } + updateStatus(useName + "@" + call.getPeerProfile().getSipDomain()); + } + + /** + * Updates whether or not the user's voice is muted, depending on whether the button is pressed. + * @param v The View where the touch event is being fired. + * @param event The motion to act on. + * @return boolean Returns false to indicate that the parent view should handle the touch event + * as it normally would. + */ + public boolean onTouch(View v, MotionEvent event) { + if (call == null) { + return false; + } else if (event.getAction() == MotionEvent.ACTION_DOWN && call != null && call.isMuted()) { + call.toggleMute(); + } else if (event.getAction() == MotionEvent.ACTION_UP && !call.isMuted()) { + call.toggleMute(); + } + return false; + } + + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(0, CALL_ADDRESS, 0, "Call someone"); + menu.add(0, SET_AUTH_INFO, 0, "Edit your SIP Info."); + menu.add(0, HANG_UP, 0, "End Current Call."); + + return true; + } + + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case CALL_ADDRESS: + showDialog(CALL_ADDRESS); + break; + case SET_AUTH_INFO: + updatePreferences(); + break; + case HANG_UP: + if(call != null) { + try { + call.endCall(); + } catch (SipException se) { + Log.d("WalkieTalkieActivity/onOptionsItemSelected", + "Error ending call.", se); + } + call.close(); + } + break; + } + return true; + } + + @Override + protected Dialog onCreateDialog(int id) { + switch (id) { + case CALL_ADDRESS: + + LayoutInflater factory = LayoutInflater.from(this); + final View textBoxView = factory.inflate(R.layout.call_address_dialog, null); + return new AlertDialog.Builder(this) + .setTitle("Call Someone.") + .setView(textBoxView) + .setPositiveButton( + android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + EditText textField = (EditText) + (textBoxView.findViewById(R.id.calladdress_edit)); + sipAddress = textField.getText().toString(); + initiateCall(); + + } + }) + .setNegativeButton( + android.R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // Noop. + } + }) + .create(); + + case UPDATE_SETTINGS_DIALOG: + return new AlertDialog.Builder(this) + .setMessage("Please update your SIP Account Settings.") + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + updatePreferences(); + } + }) + .setNegativeButton( + android.R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // Noop. + } + }) + .create(); + } + return null; + } + + public void updatePreferences() { + Intent settingsActivity = new Intent(getBaseContext(), + SipSettings.class); + startActivity(settingsActivity); + } +}