Split unit tests and interactive test apps apart
This CL splits the automated unit tests (*.tests package) and the
interactive test connection services (*.testapps package).
Apart from code hygiene and flexibility moving forward, this makes
Android manifest and build files, and the relevant dependencies,
simpler and easier to follow.
Change-Id: Id8c7763ae65f437fdfdabe8b0a4f3561adadbcb3
diff --git a/testapps/Android.mk b/testapps/Android.mk
new file mode 100644
index 0000000..9c11d34
--- /dev/null
+++ b/testapps/Android.mk
@@ -0,0 +1,31 @@
+#
+# Copyright (C) 2013 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-ex-camera2 \
+ guava
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := TelecomTestApps
+LOCAL_CERTIFICATE := platform
+
+LOCAL_MODULE_TAGS := tests
+
+include $(BUILD_PACKAGE)
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
new file mode 100644
index 0000000..747d377
--- /dev/null
+++ b/testapps/AndroidManifest.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ coreApp="true"
+ package="com.android.server.telecom.testapps">
+
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE" />
+ <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+ <uses-permission android:name="android.permission.REGISTER_CALL_PROVIDER" />
+ <uses-permission android:name="android.permission.REGISTER_CONNECTION_MANAGER" />
+ <uses-permission android:name="android.permission.REGISTER_SIM_SUBSCRIPTION" />
+
+ <application android:label="@string/app_name">
+ <uses-library android:name="android.test.runner" />
+
+ <!-- Miscellaneous telecom app-related test activities. -->
+
+ <service android:name="com.android.server.telecom.testapps.TestConnectionService"
+ android:permission="android.permission.BIND_CONNECTION_SERVICE" >
+ <intent-filter>
+ <action android:name="android.telecom.ConnectionService" />
+ </intent-filter>
+ </service>
+
+ <service android:name="com.android.server.telecom.testapps.TestConnectionManager"
+ android:permission="android.permission.BIND_CONNECTION_SERVICE" >
+ <intent-filter>
+ <action android:name="android.telecom.ConnectionService" />
+ </intent-filter>
+ </service>
+
+ <service android:name="com.android.server.telecom.testapps.TestInCallServiceImpl"
+ android:process="com.android.server.telecom.testapps.TestInCallService"
+ android:permission="android.permission.BIND_INCALL_SERVICE" >
+ <intent-filter>
+ <action android:name="android.telecom.InCallService"/>
+ </intent-filter>
+ </service>
+
+ <activity android:name="com.android.server.telecom.testapps.TestCallActivity"
+ android:label="@string/testCallActivityLabel">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.telecom.testapps.ACTION_START_INCOMING_CALL" />
+ <action android:name="android.telecom.testapps.ACTION_NEW_UNKNOWN_CALL" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="tel" />
+ <data android:scheme="sip" />
+ </intent-filter>
+ </activity>
+
+ <receiver android:name="com.android.server.telecom.testapps.CallNotificationReceiver"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="com.android.server.telecom.testapps.ACTION_CALL_SERVICE_EXIT" />
+ </intent-filter>
+ </receiver>
+
+ <activity android:name="com.android.server.telecom.testapps.TestDialerActivity"
+ android:label="@string/testDialerActivityLabel"
+ android:process="com.android.server.telecom.testapps.TestInCallService">
+ <intent-filter>
+ <action android:name="android.intent.action.DIAL" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:mimeType="vnd.android.cursor.item/phone" />
+ <data android:mimeType="vnd.android.cursor.item/person" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.DIAL" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="voicemail" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.DIAL" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <action android:name="android.intent.action.DIAL" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="tel" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/testapps/res/drawable-xhdpi/stat_sys_phone_call.png b/testapps/res/drawable-xhdpi/stat_sys_phone_call.png
new file mode 100644
index 0000000..1bb4340
--- /dev/null
+++ b/testapps/res/drawable-xhdpi/stat_sys_phone_call.png
Binary files differ
diff --git a/testapps/res/layout/testdialer_main.xml b/testapps/res/layout/testdialer_main.xml
new file mode 100644
index 0000000..a5453fc
--- /dev/null
+++ b/testapps/res/layout/testdialer_main.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+ <EditText
+ android:id="@+id/number"
+ android:inputType="number"
+ android:layout_width="200dp"
+ android:layout_height="wrap_content" />
+ <Button
+ android:id="@+id/place_call_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/placeCallButton" />
+ <Button
+ android:id="@+id/set_default_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/defaultDialerButton" />
+</LinearLayout>
diff --git a/testapps/res/raw/beep_boop.ogg b/testapps/res/raw/beep_boop.ogg
new file mode 100644
index 0000000..83148f0
--- /dev/null
+++ b/testapps/res/raw/beep_boop.ogg
Binary files differ
diff --git a/testapps/res/raw/outgoing_video.mp4 b/testapps/res/raw/outgoing_video.mp4
new file mode 100644
index 0000000..3e4f1cb
--- /dev/null
+++ b/testapps/res/raw/outgoing_video.mp4
Binary files differ
diff --git a/testapps/res/raw/test_pattern.mp4 b/testapps/res/raw/test_pattern.mp4
new file mode 100644
index 0000000..401066f
--- /dev/null
+++ b/testapps/res/raw/test_pattern.mp4
Binary files differ
diff --git a/testapps/res/raw/test_video.mp4 b/testapps/res/raw/test_video.mp4
new file mode 100644
index 0000000..1a454b3
--- /dev/null
+++ b/testapps/res/raw/test_video.mp4
Binary files differ
diff --git a/testapps/res/values/donottranslate_strings.xml b/testapps/res/values/donottranslate_strings.xml
new file mode 100644
index 0000000..91d8628
--- /dev/null
+++ b/testapps/res/values/donottranslate_strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<resources>
+ <!-- Application label -->
+ <string name="app_name">TelecommTests</string>
+
+ <!-- String for the TestCallActivity -->
+ <string name="testCallActivityLabel">Test Connection Service App</string>
+
+ <!-- String for the TestDialerActivity -->
+ <string name="testDialerActivityLabel">Test Dialer</string>
+
+ <!-- String for button in TestDialerActivity that reassigns the default Dialer -->
+ <string name="defaultDialerButton">Default dialer request</string>
+
+ <!-- String for button in TestDialerActivity that places a test call -->
+ <string name="placeCallButton">Place call</string>
+</resources>
diff --git a/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java b/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
new file mode 100644
index 0000000..a835bf1
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2013 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.android.server.telecom.testapps;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.telecom.CallState;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.util.Log;
+
+/**
+ * This class receives the notification callback intents used to update call states for
+ * {@link TestConnectionService}.
+ */
+public class CallNotificationReceiver extends BroadcastReceiver {
+
+ static final String TAG = CallNotificationReceiver.class.getSimpleName();
+ /**
+ * Exit intent action is sent when the user clicks the "exit" action of the
+ * TestConnectionService notification. Used to cancel (remove) the notification.
+ */
+ static final String ACTION_CALL_SERVICE_EXIT =
+ "com.android.server.telecom.testapps.ACTION_CALL_SERVICE_EXIT";
+ static final String ACTION_REGISTER_PHONE_ACCOUNT =
+ "com.android.server.telecom.testapps.ACTION_REGISTER_PHONE_ACCOUNT";
+ static final String ACTION_SHOW_ALL_PHONE_ACCOUNTS =
+ "com.android.server.telecom.testapps.ACTION_SHOW_ALL_PHONE_ACCOUNTS";
+ static final String ACTION_VIDEO_CALL =
+ "com.android.server.telecom.testapps.ACTION_VIDEO_CALL";
+ static final String ACTION_AUDIO_CALL =
+ "com.android.server.telecom.testapps.ACTION_AUDIO_CALL";
+
+ /** {@inheritDoc} */
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (ACTION_CALL_SERVICE_EXIT.equals(action)) {
+ CallServiceNotifier.getInstance().cancelNotifications(context);
+ } else if (ACTION_REGISTER_PHONE_ACCOUNT.equals(action)) {
+ CallServiceNotifier.getInstance().registerPhoneAccount(context);
+ } else if (ACTION_SHOW_ALL_PHONE_ACCOUNTS.equals(action)) {
+ CallServiceNotifier.getInstance().showAllPhoneAccounts(context);
+ } else if (ACTION_VIDEO_CALL.equals(action)) {
+ sendIncomingCallIntent(context, null, true);
+ } else if (ACTION_AUDIO_CALL.equals(action)) {
+ sendIncomingCallIntent(context, null, false);
+ }
+ }
+
+ /**
+ * Creates and sends the intent to add an incoming call through Telecom.
+ *
+ * @param context The current context.
+ * @param isVideoCall {@code True} if this is a video call.
+ */
+ public static void sendIncomingCallIntent(Context context, Uri handle, boolean isVideoCall) {
+ PhoneAccountHandle phoneAccount = new PhoneAccountHandle(
+ new ComponentName(context, TestConnectionService.class),
+ CallServiceNotifier.SIM_SUBSCRIPTION_ID);
+
+ // For the purposes of testing, indicate whether the incoming call is a video call by
+ // stashing an indicator in the EXTRA_INCOMING_CALL_EXTRAS.
+ Bundle extras = new Bundle();
+ extras.putBoolean(TestConnectionService.EXTRA_IS_VIDEO_CALL, isVideoCall);
+ if (handle != null) {
+ extras.putParcelable(TestConnectionService.EXTRA_HANDLE, handle);
+ }
+
+ TelecomManager.from(context).addNewIncomingCall(phoneAccount, extras);
+ }
+
+ public static void addNewUnknownCall(Context context, Uri handle, Bundle extras) {
+ Log.i(TAG, "Adding new unknown call with handle " + handle);
+ PhoneAccountHandle phoneAccount = new PhoneAccountHandle(
+ new ComponentName(context, TestConnectionService.class),
+ CallServiceNotifier.SIM_SUBSCRIPTION_ID);
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ if (handle != null) {
+ extras.putParcelable(TelecomManager.EXTRA_UNKNOWN_CALL_HANDLE, handle);
+ extras.putParcelable(TestConnectionService.EXTRA_HANDLE, handle);
+ }
+
+ TelecomManager.from(context).addNewUnknownCall(phoneAccount, extras);
+ }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/CallServiceNotifier.java b/testapps/src/com/android/server/telecom/testapps/CallServiceNotifier.java
new file mode 100644
index 0000000..d40f92d
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/CallServiceNotifier.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2013 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.android.server.telecom.testapps;
+
+import com.android.server.telecom.testapps.R;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.net.Uri;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Class used to create, update and cancel the notification used to display and update call state
+ * for {@link TestConnectionService}.
+ */
+public class CallServiceNotifier {
+ private static final CallServiceNotifier INSTANCE = new CallServiceNotifier();
+
+ static final String CALL_PROVIDER_ID = "testapps_TestConnectionService_CALL_PROVIDER_ID";
+ static final String SIM_SUBSCRIPTION_ID = "testapps_TestConnectionService_SIM_SUBSCRIPTION_ID";
+ static final String CONNECTION_MANAGER_ID =
+ "testapps_TestConnectionService_CONNECTION_MANAGER_ID";
+
+ /**
+ * Static notification IDs.
+ */
+ private static final int CALL_NOTIFICATION_ID = 1;
+ private static final int PHONE_ACCOUNT_NOTIFICATION_ID = 2;
+
+ /**
+ * Whether the added call should be started as a video call. Referenced by
+ * {@link TestConnectionService} to know whether to provide a call video provider.
+ */
+ public static boolean mStartVideoCall;
+
+ /**
+ * Singleton accessor.
+ */
+ public static CallServiceNotifier getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Creates a CallService & initializes notification manager.
+ */
+ private CallServiceNotifier() {
+ }
+
+ /**
+ * Updates the notification in the notification pane.
+ */
+ public void updateNotification(Context context) {
+ log("adding the notification ------------");
+ getNotificationManager(context).notify(CALL_NOTIFICATION_ID, getMainNotification(context));
+ getNotificationManager(context).notify(
+ PHONE_ACCOUNT_NOTIFICATION_ID, getPhoneAccountNotification(context));
+ }
+
+ /**
+ * Cancels the notification.
+ */
+ public void cancelNotifications(Context context) {
+ log("canceling notification");
+ getNotificationManager(context).cancel(CALL_NOTIFICATION_ID);
+ getNotificationManager(context).cancel(PHONE_ACCOUNT_NOTIFICATION_ID);
+ }
+
+ /**
+ * Registers a phone account with telecom.
+ */
+ public void registerPhoneAccount(Context context) {
+ TelecomManager telecomManager =
+ (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+
+ telecomManager.clearAccounts();
+
+ telecomManager.registerPhoneAccount(PhoneAccount.builder(
+ new PhoneAccountHandle(
+ new ComponentName(context, TestConnectionService.class),
+ CALL_PROVIDER_ID),
+ "TelecomTestApp Call Provider")
+ .setAddress(Uri.parse("tel:555-TEST"))
+ .setSubscriptionAddress(Uri.parse("tel:555-TEST"))
+ .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER |
+ PhoneAccount.CAPABILITY_VIDEO_CALLING)
+ .setIcon(context, R.drawable.stat_sys_phone_call, Color.RED)
+ .setHighlightColor(Color.RED)
+ .setShortDescription("a short description for the call provider")
+ .setSupportedUriSchemes(Arrays.asList("tel"))
+ .build());
+
+ telecomManager.registerPhoneAccount(PhoneAccount.builder(
+ new PhoneAccountHandle(
+ new ComponentName(context, TestConnectionService.class),
+ SIM_SUBSCRIPTION_ID),
+ "TelecomTestApp SIM Subscription")
+ .setAddress(Uri.parse("tel:555-TSIM"))
+ .setSubscriptionAddress(Uri.parse("tel:555-TSIM"))
+ .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER |
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION |
+ PhoneAccount.CAPABILITY_VIDEO_CALLING)
+ .setIcon(context, R.drawable.stat_sys_phone_call, Color.GREEN)
+ .setHighlightColor(Color.GREEN)
+ .setShortDescription("a short description for the sim subscription")
+ .build());
+
+ telecomManager.registerPhoneAccount(PhoneAccount.builder(
+ new PhoneAccountHandle(
+ new ComponentName(context, TestConnectionManager.class),
+ CONNECTION_MANAGER_ID),
+ "TelecomTestApp CONNECTION MANAGER")
+ .setAddress(Uri.parse("tel:555-CMGR"))
+ .setSubscriptionAddress(Uri.parse("tel:555-CMGR"))
+ .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER)
+ .setIcon(context, R.drawable.stat_sys_phone_call, Color.BLUE)
+ .setShortDescription("a short description for the connection manager")
+ .build());
+ }
+
+ /**
+ * Displays all phone accounts registered with telecom.
+ */
+ public void showAllPhoneAccounts(Context context) {
+ TelecomManager telecomManager =
+ (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+ List<PhoneAccountHandle> accounts = telecomManager.getCallCapablePhoneAccounts();
+
+ Toast.makeText(context, accounts.toString(), Toast.LENGTH_LONG).show();
+ }
+
+ /**
+ * Returns the system's notification manager needed to add/remove notifications.
+ */
+ private NotificationManager getNotificationManager(Context context) {
+ return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ /**
+ * Creates a notification object for using the telecom APIs.
+ */
+ private Notification getPhoneAccountNotification(Context context) {
+ final Notification.Builder builder = new Notification.Builder(context);
+ // Both notifications have buttons and only the first one with buttons will show its
+ // buttons. Since the phone accounts notification is always first, setting false ensures
+ // it can be dismissed to use the other notification.
+ builder.setOngoing(false);
+ builder.setPriority(Notification.PRIORITY_HIGH);
+
+ final PendingIntent intent = createShowAllPhoneAccountsIntent(context);
+ builder.setContentIntent(intent);
+
+ builder.setSmallIcon(android.R.drawable.stat_sys_phone_call);
+ // TODO: Consider moving this into a strings.xml
+ builder.setContentText("Test phone accounts via telecom APIs.");
+ builder.setContentTitle("Test Phone Accounts");
+
+ addRegisterPhoneAccountAction(builder, context);
+ addShowAllPhoneAccountsAction(builder, context);
+
+ return builder.build();
+ }
+
+ /**
+ * Creates a notification object out of the current calls state.
+ */
+ private Notification getMainNotification(Context context) {
+ final Notification.Builder builder = new Notification.Builder(context);
+ builder.setOngoing(true);
+ builder.setPriority(Notification.PRIORITY_HIGH);
+ builder.setSmallIcon(android.R.drawable.stat_sys_phone_call);
+ builder.setContentText("Test calls via CallService API");
+ builder.setContentTitle("Test Connection Service");
+
+ addAddVideoCallAction(builder, context);
+ addAddCallAction(builder, context);
+ addExitAction(builder, context);
+
+ return builder.build();
+ }
+
+ /**
+ * Creates the intent to remove the notification.
+ */
+ private PendingIntent createExitIntent(Context context) {
+ final Intent intent = new Intent(CallNotificationReceiver.ACTION_CALL_SERVICE_EXIT, null,
+ context, CallNotificationReceiver.class);
+
+ return PendingIntent.getBroadcast(context, 0, intent, 0);
+ }
+
+ /**
+ * Creates the intent to register a phone account.
+ */
+ private PendingIntent createRegisterPhoneAccountIntent(Context context) {
+ final Intent intent = new Intent(CallNotificationReceiver.ACTION_REGISTER_PHONE_ACCOUNT,
+ null, context, CallNotificationReceiver.class);
+ return PendingIntent.getBroadcast(context, 0, intent, 0);
+ }
+
+ /**
+ * Creates the intent to show all phone accounts.
+ */
+ private PendingIntent createShowAllPhoneAccountsIntent(Context context) {
+ final Intent intent = new Intent(CallNotificationReceiver.ACTION_SHOW_ALL_PHONE_ACCOUNTS,
+ null, context, CallNotificationReceiver.class);
+ return PendingIntent.getBroadcast(context, 0, intent, 0);
+ }
+
+ /**
+ * Creates the intent to start an incoming video call
+ */
+ private PendingIntent createIncomingVideoCall(Context context) {
+ final Intent intent = new Intent(CallNotificationReceiver.ACTION_VIDEO_CALL,
+ null, context, CallNotificationReceiver.class);
+ return PendingIntent.getBroadcast(context, 0, intent, 0);
+ }
+
+ /**
+ * Creates the intent to start an incoming audio call
+ */
+ private PendingIntent createIncomingAudioCall(Context context) {
+ final Intent intent = new Intent(CallNotificationReceiver.ACTION_AUDIO_CALL,
+ null, context, CallNotificationReceiver.class);
+ return PendingIntent.getBroadcast(context, 0, intent, 0);
+ }
+
+ /**
+ * Adds an action to the Notification Builder for adding an incoming call through Telecom.
+ * @param builder The Notification Builder.
+ */
+ private void addAddCallAction(Notification.Builder builder, Context context) {
+ builder.addAction(0, "Add Call", createIncomingAudioCall(context));
+ }
+
+ /**
+ * Adds an action to the Notification Builder to add an incoming video call through Telecom.
+ */
+ private void addAddVideoCallAction(Notification.Builder builder, Context context) {
+ builder.addAction(0, "Add Video", createIncomingVideoCall(context));
+ }
+
+ /**
+ * Adds an action to remove the notification.
+ */
+ private void addExitAction(Notification.Builder builder, Context context) {
+ builder.addAction(0, "Exit", createExitIntent(context));
+ }
+
+ /**
+ * Adds an action to show all registered phone accounts on a device.
+ */
+ private void addShowAllPhoneAccountsAction(Notification.Builder builder, Context context) {
+ builder.addAction(0, "Show Accts", createShowAllPhoneAccountsIntent(context));
+ }
+
+ /**
+ * Adds an action to register a new phone account.
+ */
+ private void addRegisterPhoneAccountAction(Notification.Builder builder, Context context) {
+ builder.addAction(0, "Reg.Acct.", createRegisterPhoneAccountIntent(context));
+ }
+
+ public boolean shouldStartVideoCall() {
+ return mStartVideoCall;
+ }
+
+ private static void log(String msg) {
+ Log.w("testcallservice", "[CallServiceNotifier] " + msg);
+ }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/CameraThread.java b/testapps/src/com/android/server/telecom/testapps/CameraThread.java
new file mode 100644
index 0000000..fbcd6b2
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/CameraThread.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2014 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.android.server.telecom.testapps;
+
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.lang.AutoCloseable;
+import java.lang.Exception;
+import java.lang.Override;
+import java.lang.String;
+import java.lang.Thread;
+import java.lang.Throwable;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Camera thread class used for handling camera callbacks.
+ */
+public class CameraThread implements AutoCloseable {
+ private static final String TAG = "CameraThread";
+ private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
+
+ // Timeout for initializing looper and opening camera in Milliseconds.
+ private static final long WAIT_FOR_COMMAND_TO_COMPLETE = 5000;
+ private Looper mLooper = null;
+ private Handler mHandler = null;
+
+ /**
+ * Create and start a looper thread, return the Handler
+ */
+ public synchronized Handler start() throws Exception {
+ final ConditionVariable startDone = new ConditionVariable();
+ if (mHandler != null) {
+ Log.w(TAG, "Looper thread already started");
+ return mHandler;
+ }
+
+ new Thread() {
+ @Override
+ public void run() {
+ if (VERBOSE) Log.v(TAG, "start loopRun");
+ Looper.prepare();
+ // Save the looper so that we can terminate this thread
+ // after we are done with it.
+ mLooper = Looper.myLooper();
+ mHandler = new Handler();
+ startDone.open();
+ Looper.loop();
+ if (VERBOSE) Log.v(TAG, "createLooperThread: finished");
+ }
+ }.start();
+
+ if (VERBOSE) Log.v(TAG, "start waiting for looper");
+ if (!startDone.block(WAIT_FOR_COMMAND_TO_COMPLETE)) {
+ throw new TimeoutException("createLooperThread: start timeout");
+ }
+ return mHandler;
+ }
+
+ /**
+ * Terminate the looper thread
+ */
+ public synchronized void close() throws Exception {
+ if (mLooper == null || mHandler == null) {
+ Log.w(TAG, "Looper thread doesn't start yet");
+ return;
+ }
+
+ if (VERBOSE) Log.v(TAG, "Terminate looper thread");
+ mLooper.quit();
+ mLooper.getThread().join();
+ mLooper = null;
+ mHandler = null;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java b/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java
new file mode 100644
index 0000000..6f4ae20
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2013 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.android.server.telecom.testapps;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+
+/**
+ * This activity exists in order to add an icon to the launcher. This activity has no UI of its own
+ * and instead starts the notification for {@link TestConnectionService} via
+ * {@link CallServiceNotifier}. After triggering a notification update, this activity immediately
+ * finishes.
+ *
+ * To directly trigger a new incoming call, use the following adb command:
+ *
+ * adb shell am start -a android.telecom.testapps.ACTION_START_INCOMING_CALL -d "tel:123456789"
+ */
+public class TestCallActivity extends Activity {
+
+ public static final String ACTION_NEW_INCOMING_CALL =
+ "android.telecom.testapps.ACTION_START_INCOMING_CALL";
+
+ /*
+ * Action to exercise TelecomManager.addNewUnknownCall().
+ */
+ public static final String ACTION_NEW_UNKNOWN_CALL =
+ "android.telecom.testapps.ACTION_NEW_UNKNOWN_CALL";
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ final Intent intent = getIntent();
+ final String action = intent != null ? intent.getAction() : null;
+ final Uri data = intent != null ? intent.getData() : null;
+ if (ACTION_NEW_INCOMING_CALL.equals(action) && data != null) {
+ CallNotificationReceiver.sendIncomingCallIntent(this, data, false);
+ } else if (ACTION_NEW_UNKNOWN_CALL.equals(action) && data != null) {
+ CallNotificationReceiver.addNewUnknownCall(this, data, intent.getExtras());
+ } else {
+ CallServiceNotifier.getInstance().updateNotification(this);
+ }
+ finish();
+ }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestConnectionManager.java b/testapps/src/com/android/server/telecom/testapps/TestConnectionManager.java
new file mode 100644
index 0000000..a27be39
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestConnectionManager.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2013 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.android.server.telecom.testapps;
+
+import android.net.Uri;
+import android.telecom.AudioState;
+import android.telecom.Conference;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.RemoteConference;
+import android.telecom.RemoteConnection;
+import android.telecom.StatusHints;
+import android.telecom.VideoProfile;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Service which acts as a fake ConnectionManager if so configured.
+ * TODO(santoscordon): Rename all classes in the directory to Dummy* (e.g., DummyConnectionService).
+ */
+public class TestConnectionManager extends ConnectionService {
+ public final class TestManagedConnection extends Connection {
+ private final RemoteConnection.Callback mRemoteCallback = new RemoteConnection.Callback() {
+ @Override
+ public void onStateChanged(RemoteConnection connection, int state) {
+ setState(state);
+ }
+
+ @Override
+ public void onDisconnected(
+ RemoteConnection connection, DisconnectCause disconnectCause) {
+ setDisconnected(disconnectCause);
+ destroy();
+ }
+
+ @Override
+ public void onRingbackRequested(RemoteConnection connection, boolean ringback) {
+ setRingbackRequested(ringback);
+ }
+
+ @Override
+ public void onConnectionCapabilitiesChanged(RemoteConnection connection,
+ int connectionCapabilities) {
+ setConnectionCapabilities(connectionCapabilities);
+ }
+
+ @Override
+ public void onPostDialWait(RemoteConnection connection, String remainingDigits) {
+ setPostDialWait(remainingDigits);
+ }
+
+ @Override
+ public void onVoipAudioChanged(RemoteConnection connection, boolean isVoip) {
+ setAudioModeIsVoip(isVoip);
+ }
+
+ @Override
+ public void onStatusHintsChanged(RemoteConnection connection, StatusHints statusHints) {
+ setStatusHints(statusHints);
+ }
+
+ @Override
+ public void onVideoStateChanged(RemoteConnection connection, int videoState) {
+ if (videoState == VideoProfile.VideoState.BIDIRECTIONAL) {
+ setVideoProvider(new TestManagedVideoProvider(connection.getVideoProvider()));
+ }
+ setVideoState(videoState);
+ }
+
+ @Override
+ public void onAddressChanged(
+ RemoteConnection connection, Uri address, int presentation) {
+ setAddress(address, presentation);
+ }
+
+ @Override
+ public void onCallerDisplayNameChanged(
+ RemoteConnection connection, String callerDisplayName, int presentation) {
+ setCallerDisplayName(callerDisplayName, presentation);
+ }
+
+ @Override
+ public void onDestroyed(RemoteConnection connection) {
+ destroy();
+ mManagedConnectionByRemote.remove(mRemote);
+ }
+
+ @Override
+ public void onConferenceableConnectionsChanged(
+ RemoteConnection connect,
+ List<RemoteConnection> conferenceable) {
+ List<Connection> c = new ArrayList<>();
+ for (RemoteConnection remote : conferenceable) {
+ if (mManagedConnectionByRemote.containsKey(remote)) {
+ c.add(mManagedConnectionByRemote.get(remote));
+ }
+ }
+ setConferenceableConnections(c);
+ }
+ };
+
+ private final RemoteConnection mRemote;
+ private final boolean mIsIncoming;
+
+ TestManagedConnection(RemoteConnection remote, boolean isIncoming) {
+ mRemote = remote;
+ mIsIncoming = isIncoming;
+ mRemote.registerCallback(mRemoteCallback);
+ setState(mRemote.getState());
+ setVideoState(mRemote.getVideoState());
+ }
+
+ @Override
+ public void onAbort() {
+ mRemote.abort();
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onAnswer(int videoState) {
+ mRemote.answer(videoState);
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onDisconnect() {
+ mRemote.disconnect();
+ }
+
+ @Override
+ public void onPlayDtmfTone(char c) {
+ mRemote.playDtmfTone(c);
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onHold() {
+ mRemote.hold();
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onReject() {
+ mRemote.reject();
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onUnhold() {
+ mRemote.unhold();
+ }
+
+ @Override
+ public void onAudioStateChanged(AudioState state) {
+ mRemote.setAudioState(state);
+ }
+
+ private void setState(int state) {
+ log("setState: " + state);
+ switch (state) {
+ case STATE_ACTIVE:
+ setActive();
+ break;
+ case STATE_HOLDING:
+ setOnHold();
+ break;
+ case STATE_DIALING:
+ setDialing();
+ break;
+ case STATE_RINGING:
+ setRinging();
+ break;
+ }
+ }
+ }
+
+ public final class TestManagedConference extends Conference {
+ private final RemoteConference.Callback mRemoteCallback = new RemoteConference.Callback() {
+ @Override
+ public void onStateChanged(RemoteConference conference, int oldState, int newState) {
+ switch (newState) {
+ case Connection.STATE_DISCONNECTED:
+ // See onDisconnected below
+ break;
+ case Connection.STATE_HOLDING:
+ setOnHold();
+ break;
+ case Connection.STATE_ACTIVE:
+ setActive();
+ break;
+ default:
+ log("unrecognized state for Conference: " + newState);
+ break;
+ }
+ }
+
+ @Override
+ public void onDisconnected(RemoteConference conference,
+ DisconnectCause disconnectCause) {
+ setDisconnected(disconnectCause);
+ }
+
+ @Override
+ public void onConnectionAdded(
+ RemoteConference conference,
+ RemoteConnection connection) {
+ TestManagedConnection c = mManagedConnectionByRemote.get(connection);
+ if (c == null) {
+ log("onConnectionAdded cannot find remote connection: " + connection);
+ } else {
+ addConnection(c);
+ }
+ }
+
+ @Override
+ public void onConnectionRemoved(
+ RemoteConference conference,
+ RemoteConnection connection) {
+ TestManagedConnection c = mManagedConnectionByRemote.get(connection);
+ if (c == null) {
+ log("onConnectionRemoved cannot find remote connection: " + connection);
+ } else {
+ removeConnection(c);
+ }
+ }
+
+ @Override
+ public void onConnectionCapabilitiesChanged(RemoteConference conference,
+ int connectionCapabilities) {
+ setConnectionCapabilities(connectionCapabilities);
+ }
+
+ @Override
+ public void onDestroyed(RemoteConference conference) {
+ destroy();
+ mRemote.unregisterCallback(mRemoteCallback);
+ mManagedConferenceByRemote.remove(mRemote);
+ }
+
+ };
+
+ @Override
+ public void onPlayDtmfTone(char c) {
+ mRemote.playDtmfTone(c);
+ };
+
+ @Override
+ public void onStopDtmfTone() {
+ mRemote.stopDtmfTone();
+ };
+
+ private final RemoteConference mRemote;
+
+ public TestManagedConference(RemoteConference remote) {
+ super(null);
+ mRemote = remote;
+ remote.registerCallback(mRemoteCallback);
+ setActive();
+ for (RemoteConnection r : remote.getConnections()) {
+ TestManagedConnection c = mManagedConnectionByRemote.get(r);
+ if (c != null) {
+ addConnection(c);
+ }
+ }
+ }
+ }
+
+ static void log(String msg) {
+ Log.w("telecomtestcs", "[TestConnectionManager] " + msg);
+ }
+
+ private final Map<RemoteConference, TestManagedConference> mManagedConferenceByRemote
+ = new HashMap<>();
+ private final Map<RemoteConnection, TestManagedConnection> mManagedConnectionByRemote
+ = new HashMap<>();
+
+ @Override
+ public Connection onCreateOutgoingConnection(
+ PhoneAccountHandle connectionManagerAccount,
+ final ConnectionRequest request) {
+ return makeConnection(request, false);
+ }
+
+ @Override
+ public Connection onCreateIncomingConnection(
+ PhoneAccountHandle connectionManagerAccount,
+ final ConnectionRequest request) {
+ return makeConnection(request, true);
+ }
+
+ @Override
+ public void onConference(Connection a, Connection b) {
+ conferenceRemoteConnections(
+ ((TestManagedConnection) a).mRemote,
+ ((TestManagedConnection) b).mRemote);
+ }
+
+ @Override
+ public void onRemoteConferenceAdded(RemoteConference remoteConference) {
+ addConference(new TestManagedConference(remoteConference));
+ }
+
+ Map<RemoteConnection, TestManagedConnection> getManagedConnectionByRemote() {
+ return mManagedConnectionByRemote;
+ }
+
+ private Connection makeConnection(ConnectionRequest request, boolean incoming) {
+ RemoteConnection remote = incoming
+ ? createRemoteIncomingConnection(request.getAccountHandle(), request)
+ : createRemoteOutgoingConnection(request.getAccountHandle(), request);
+ TestManagedConnection local = new TestManagedConnection(remote, false);
+ mManagedConnectionByRemote.put(remote, local);
+ return local;
+ }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java b/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
new file mode 100644
index 0000000..d71ef9d
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2013 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.android.server.telecom.testapps;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.telecom.AudioState;
+import android.telecom.Conference;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.util.Log;
+
+import com.android.server.telecom.testapps.R;
+
+import java.lang.String;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Service which provides fake calls to test the ConnectionService interface.
+ * TODO: Rename all classes in the directory to Dummy* (e.g., DummyConnectionService).
+ */
+public class TestConnectionService extends ConnectionService {
+ /**
+ * Intent extra used to pass along whether a call is video or audio based on the user's choice
+ * in the notification.
+ */
+ public static final String EXTRA_IS_VIDEO_CALL = "extra_is_video_call";
+
+ public static final String EXTRA_HANDLE = "extra_handle";
+
+ /**
+ * Random number generator used to generate phone numbers.
+ */
+ private Random mRandom = new Random();
+
+ private final class TestConference extends Conference {
+
+ private final Connection.Listener mConnectionListener = new Connection.Listener() {
+ @Override
+ public void onDestroyed(Connection c) {
+ removeConnection(c);
+ if (getConnections().size() == 0) {
+ setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
+ destroy();
+ }
+ }
+ };
+
+ public TestConference(Connection a, Connection b) {
+ super(null);
+ setConnectionCapabilities(
+ Connection.CAPABILITY_SUPPORT_HOLD |
+ Connection.CAPABILITY_HOLD |
+ Connection.CAPABILITY_MUTE |
+ Connection.CAPABILITY_MANAGE_CONFERENCE);
+ addConnection(a);
+ addConnection(b);
+
+ a.addConnectionListener(mConnectionListener);
+ b.addConnectionListener(mConnectionListener);
+
+ a.setConference(this);
+ b.setConference(this);
+
+ setActive();
+ }
+
+ @Override
+ public void onDisconnect() {
+ for (Connection c : getConnections()) {
+ c.setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
+ c.destroy();
+ }
+ }
+
+ @Override
+ public void onSeparate(Connection connection) {
+ if (getConnections().contains(connection)) {
+ connection.setConference(null);
+ removeConnection(connection);
+ connection.removeConnectionListener(mConnectionListener);
+ }
+ }
+
+ @Override
+ public void onHold() {
+ for (Connection c : getConnections()) {
+ c.setOnHold();
+ }
+ setOnHold();
+ }
+
+ @Override
+ public void onUnhold() {
+ for (Connection c : getConnections()) {
+ c.setActive();
+ }
+ setActive();
+ }
+ }
+
+ private final class TestConnection extends Connection {
+ private final boolean mIsIncoming;
+
+ /** Used to cleanup camera and media when done with connection. */
+ private TestVideoProvider mTestVideoCallProvider;
+
+ TestConnection(boolean isIncoming) {
+ mIsIncoming = isIncoming;
+ // Assume all calls are video capable.
+ int capabilities = getConnectionCapabilities();
+ capabilities |= CAPABILITY_SUPPORTS_VT_LOCAL;
+ capabilities |= CAPABILITY_MUTE;
+ capabilities |= CAPABILITY_SUPPORT_HOLD;
+ capabilities |= CAPABILITY_HOLD;
+ setConnectionCapabilities(capabilities);
+ }
+
+ void startOutgoing() {
+ setDialing();
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ setActive();
+ activateCall(TestConnection.this);
+ }
+ }, 4000);
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onAbort() {
+ destroyCall(this);
+ destroy();
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onAnswer(int videoState) {
+ setVideoState(videoState);
+ activateCall(this);
+ setActive();
+ updateConferenceable();
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onPlayDtmfTone(char c) {
+ if (c == '1') {
+ setDialing();
+ }
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onStopDtmfTone() { }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onDisconnect() {
+ setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
+ destroyCall(this);
+ destroy();
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onHold() {
+ setOnHold();
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onReject() {
+ setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
+ destroyCall(this);
+ destroy();
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onUnhold() {
+ setActive();
+ }
+
+ @Override
+ public void onAudioStateChanged(AudioState state) { }
+
+ public void setTestVideoCallProvider(TestVideoProvider testVideoCallProvider) {
+ mTestVideoCallProvider = testVideoCallProvider;
+ }
+
+ /**
+ * Stops playback of test videos.
+ */
+ private void stopAndCleanupMedia() {
+ if (mTestVideoCallProvider != null) {
+ mTestVideoCallProvider.stopAndCleanupMedia();
+ mTestVideoCallProvider.stopCamera();
+ }
+ }
+ }
+
+ private final List<TestConnection> mCalls = new ArrayList<>();
+ private final Handler mHandler = new Handler();
+
+ /** Used to play an audio tone during a call. */
+ private MediaPlayer mMediaPlayer;
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ log("onUnbind");
+ mMediaPlayer = null;
+ return super.onUnbind(intent);
+ }
+
+ @Override
+ public void onConference(Connection a, Connection b) {
+ addConference(new TestConference(a, b));
+ }
+
+ @Override
+ public Connection onCreateOutgoingConnection(
+ PhoneAccountHandle connectionManagerAccount,
+ final ConnectionRequest originalRequest) {
+
+ final Uri handle = originalRequest.getAddress();
+ String number = originalRequest.getAddress().getSchemeSpecificPart();
+ log("call, number: " + number);
+
+ // Crash on 555-DEAD to test call service crashing.
+ if ("5550340".equals(number)) {
+ throw new RuntimeException("Goodbye, cruel world.");
+ }
+
+ Bundle extras = originalRequest.getExtras();
+ String gatewayPackage = extras.getString(TelecomManager.GATEWAY_PROVIDER_PACKAGE);
+ Uri originalHandle = extras.getParcelable(TelecomManager.GATEWAY_ORIGINAL_ADDRESS);
+
+ log("gateway package [" + gatewayPackage + "], original handle [" +
+ originalHandle + "]");
+
+ final TestConnection connection = new TestConnection(false /* isIncoming */);
+ connection.setAddress(handle, TelecomManager.PRESENTATION_ALLOWED);
+
+ // If the number starts with 555, then we handle it ourselves. If not, then we
+ // use a remote connection service.
+ // TODO: Have a special phone number to test the account-picker dialog flow.
+ if (number != null && number.startsWith("555")) {
+ // Normally we would use the original request as is, but for testing purposes, we are
+ // adding ".." to the end of the number to follow its path more easily through the logs.
+ final ConnectionRequest request = new ConnectionRequest(
+ originalRequest.getAccountHandle(),
+ Uri.fromParts(handle.getScheme(),
+ handle.getSchemeSpecificPart() + "..", ""),
+ originalRequest.getExtras(),
+ originalRequest.getVideoState());
+
+ addCall(connection);
+ connection.startOutgoing();
+
+ for (Connection c : getAllConnections()) {
+ c.setOnHold();
+ }
+ } else {
+ log("Not a test number");
+ }
+ return connection;
+ }
+
+ @Override
+ public Connection onCreateIncomingConnection(
+ PhoneAccountHandle connectionManagerAccount,
+ final ConnectionRequest request) {
+ PhoneAccountHandle accountHandle = request.getAccountHandle();
+ ComponentName componentName = new ComponentName(this, TestConnectionService.class);
+
+ if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) {
+ final TestConnection connection = new TestConnection(true);
+ // Get the stashed intent extra that determines if this is a video call or audio call.
+ Bundle extras = request.getExtras();
+ boolean isVideoCall = extras.getBoolean(EXTRA_IS_VIDEO_CALL);
+ Uri providedHandle = extras.getParcelable(EXTRA_HANDLE);
+
+ // Use dummy number for testing incoming calls.
+ Uri address = providedHandle == null ?
+ Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber(isVideoCall), null)
+ : providedHandle;
+ if (isVideoCall) {
+ TestVideoProvider testVideoCallProvider =
+ new TestVideoProvider(getApplicationContext());
+ connection.setVideoProvider(testVideoCallProvider);
+
+ // Keep reference to original so we can clean up the media players later.
+ connection.setTestVideoCallProvider(testVideoCallProvider);
+ }
+
+ int videoState = isVideoCall ?
+ VideoProfile.VideoState.BIDIRECTIONAL :
+ VideoProfile.VideoState.AUDIO_ONLY;
+ connection.setVideoState(videoState);
+ connection.setAddress(address, TelecomManager.PRESENTATION_ALLOWED);
+
+ addCall(connection);
+
+ ConnectionRequest newRequest = new ConnectionRequest(
+ request.getAccountHandle(),
+ address,
+ request.getExtras(),
+ videoState);
+ connection.setVideoState(videoState);
+ return connection;
+ } else {
+ return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR,
+ "Invalid inputs: " + accountHandle + " " + componentName));
+ }
+ }
+
+ @Override
+ public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount,
+ final ConnectionRequest request) {
+ PhoneAccountHandle accountHandle = request.getAccountHandle();
+ ComponentName componentName = new ComponentName(this, TestConnectionService.class);
+ if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) {
+ final TestConnection connection = new TestConnection(false);
+ final Bundle extras = request.getExtras();
+ final Uri providedHandle = extras.getParcelable(EXTRA_HANDLE);
+
+ Uri handle = providedHandle == null ?
+ Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber(false), null)
+ : providedHandle;
+
+ connection.setAddress(handle, TelecomManager.PRESENTATION_ALLOWED);
+ connection.setDialing();
+
+ addCall(connection);
+ return connection;
+ } else {
+ return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR,
+ "Invalid inputs: " + accountHandle + " " + componentName));
+ }
+ }
+
+ private void activateCall(TestConnection connection) {
+ if (mMediaPlayer == null) {
+ mMediaPlayer = createMediaPlayer();
+ }
+ if (!mMediaPlayer.isPlaying()) {
+ mMediaPlayer.start();
+ }
+ }
+
+ private void destroyCall(TestConnection connection) {
+ mCalls.remove(connection);
+
+ // Ensure any playing media and camera resources are released.
+ connection.stopAndCleanupMedia();
+
+ // Stops audio if there are no more calls.
+ if (mCalls.isEmpty() && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+ mMediaPlayer.stop();
+ mMediaPlayer.release();
+ mMediaPlayer = createMediaPlayer();
+ }
+
+ updateConferenceable();
+ }
+
+ private void addCall(TestConnection connection) {
+ mCalls.add(connection);
+ updateConferenceable();
+ }
+
+ private void updateConferenceable() {
+ List<Connection> freeConnections = new ArrayList<>();
+ freeConnections.addAll(mCalls);
+ for (int i = 0; i < freeConnections.size(); i++) {
+ if (freeConnections.get(i).getConference() != null) {
+ freeConnections.remove(i);
+ }
+ }
+ for (int i = 0; i < freeConnections.size(); i++) {
+ Connection c = freeConnections.remove(i);
+ c.setConferenceableConnections(freeConnections);
+ freeConnections.add(i, c);
+ }
+ }
+
+ private MediaPlayer createMediaPlayer() {
+ // Prepare the media player to play a tone when there is a call.
+ MediaPlayer mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.beep_boop);
+ mediaPlayer.setLooping(true);
+ return mediaPlayer;
+ }
+
+ private static void log(String msg) {
+ Log.w("telecomtestcs", "[TestConnectionService] " + msg);
+ }
+
+ /**
+ * Generates a random phone number of format 555YXXX. Where Y will be {@code 1} if the
+ * phone number is for a video call and {@code 0} for an audio call. XXX is a randomly
+ * generated phone number.
+ *
+ * @param isVideo {@code True} if the call is a video call.
+ * @return The phone number.
+ */
+ private String getDummyNumber(boolean isVideo) {
+ int videoDigit = isVideo ? 1 : 0;
+ int number = mRandom.nextInt(999);
+ return String.format("555%s%03d", videoDigit, number);
+ }
+}
+
diff --git a/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java b/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
new file mode 100644
index 0000000..71c375a
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
@@ -0,0 +1,56 @@
+package com.android.server.telecom.testapps;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.EditText;
+
+import com.android.server.telecom.testapps.R;
+
+public class TestDialerActivity extends Activity {
+ private EditText mNumberView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.testdialer_main);
+ findViewById(R.id.set_default_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setDefault();
+ }
+ });
+ findViewById(R.id.place_call_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ placeCall();
+ }
+ });
+
+ mNumberView = (EditText) findViewById(R.id.number);
+ updateEditTextWithNumber();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ updateEditTextWithNumber();
+ }
+
+ private void updateEditTextWithNumber() {
+ Intent intent = getIntent();
+ if (intent != null) {
+ mNumberView.setText(intent.getDataString());
+ }
+ }
+
+ private void setDefault() {
+ // TODO: Send a request to become the default dialer application
+ }
+
+ private void placeCall() {
+ // TODO: Place a call with the number entered in the number field
+ }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestInCallServiceImpl.java b/testapps/src/com/android/server/telecom/testapps/TestInCallServiceImpl.java
new file mode 100644
index 0000000..1d7e805
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestInCallServiceImpl.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2014 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.android.server.telecom.testapps;
+
+import android.telecom.InCallService;
+import android.telecom.Phone;
+import android.util.Log;
+
+import java.lang.Override;
+import java.lang.String;
+
+/**
+ * Test In-Call service implementation. Logs incoming events. Mainly used to test binding to
+ * multiple {@link InCallService} implementations.
+ */
+public class TestInCallServiceImpl extends InCallService {
+ private static final String TAG = "TestInCallServiceImpl";
+
+ private Phone mPhone;
+
+ private Phone.Listener mPhoneListener = new Phone.Listener() {
+ @Override
+ public void onCallAdded(Phone phone, android.telecom.Call call) {
+ Log.i(TAG, "onCallAdded: "+call.toString());
+ }
+ @Override
+ public void onCallRemoved(Phone phone, android.telecom.Call call) {
+ Log.i(TAG, "onCallRemoved: "+call.toString());
+ }
+ };
+
+ @Override
+ public void onPhoneCreated(Phone phone) {
+ Log.i(TAG, "onPhoneCreated");
+ mPhone = phone;
+ mPhone.addListener(mPhoneListener);
+
+ }
+
+ @Override
+ public void onPhoneDestroyed(Phone phone) {
+ Log.i(TAG, "onPhoneDestroyed");
+ mPhone.removeListener(mPhoneListener);
+ mPhone = null;
+ }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestManagedVideoProvider.java b/testapps/src/com/android/server/telecom/testapps/TestManagedVideoProvider.java
new file mode 100644
index 0000000..649d0c0
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestManagedVideoProvider.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2014 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.android.server.telecom.testapps;
+
+import android.telecom.CameraCapabilities;
+import android.telecom.Connection;
+import android.telecom.RemoteConnection;
+import android.telecom.VideoProfile;
+import android.view.Surface;
+
+public class TestManagedVideoProvider extends Connection.VideoProvider {
+
+ private final RemoteConnection.VideoProvider.Listener mRemoteListener =
+ new RemoteConnection.VideoProvider.Listener() {
+ @Override
+ public void onReceiveSessionModifyRequest(RemoteConnection.VideoProvider rvp,
+ VideoProfile videoProfile) {
+ super.onReceiveSessionModifyRequest(rvp, videoProfile);
+ }
+
+ @Override
+ public void onReceiveSessionModifyResponse(RemoteConnection.VideoProvider rvp,
+ int status,
+ VideoProfile requestedProfile, VideoProfile responseProfile) {
+ super.onReceiveSessionModifyResponse(rvp, status, requestedProfile,
+ responseProfile);
+ }
+
+ @Override
+ public void onHandleCallSessionEvent(RemoteConnection.VideoProvider rvp, int event) {
+ super.onHandleCallSessionEvent(rvp, event);
+ }
+
+ @Override
+ public void onPeerDimensionsChanged(RemoteConnection.VideoProvider rvp, int width,
+ int height) {
+ super.onPeerDimensionsChanged(rvp, width, height);
+ }
+
+ @Override
+ public void onCallDataUsageChanged(RemoteConnection.VideoProvider rvp, int dataUsage) {
+ super.onCallDataUsageChanged(rvp, dataUsage);
+ }
+
+ @Override
+ public void onCameraCapabilitiesChanged(RemoteConnection.VideoProvider rvp,
+ CameraCapabilities cameraCapabilities) {
+ super.onCameraCapabilitiesChanged(rvp, cameraCapabilities);
+ }
+ };
+
+ private final RemoteConnection.VideoProvider mRemoteVideoProvider;
+
+ public TestManagedVideoProvider(RemoteConnection.VideoProvider remoteVideoProvider) {
+ mRemoteVideoProvider = remoteVideoProvider;
+ mRemoteVideoProvider.addListener(mRemoteListener);
+ }
+
+ @Override
+ public void onSetCamera(String cameraId) {
+ mRemoteVideoProvider.setCamera(cameraId);
+ }
+
+ @Override
+ public void onSetPreviewSurface(Surface surface) {
+ mRemoteVideoProvider.setPreviewSurface(surface);
+ }
+
+ @Override
+ public void onSetDisplaySurface(Surface surface) {
+ mRemoteVideoProvider.setDisplaySurface(surface);
+ }
+
+ @Override
+ public void onSetDeviceOrientation(int rotation) {
+ mRemoteVideoProvider.setDeviceOrientation(rotation);
+ }
+
+ @Override
+ public void onSetZoom(float value) {
+ mRemoteVideoProvider.setZoom(value);
+ }
+
+ @Override
+ public void onSendSessionModifyRequest(VideoProfile requestProfile) {
+ mRemoteVideoProvider.sendSessionModifyRequest(requestProfile);
+ }
+
+ @Override
+ public void onSendSessionModifyResponse(VideoProfile responseProfile) {
+ mRemoteVideoProvider.sendSessionModifyResponse(responseProfile);
+ }
+
+ @Override
+ public void onRequestCameraCapabilities() {
+ mRemoteVideoProvider.requestCameraCapabilities();
+ }
+
+ @Override
+ public void onRequestConnectionDataUsage() {
+ mRemoteVideoProvider.requestCallDataUsage();
+ }
+
+ @Override
+ public void onSetPauseImage(String uri) {
+ mRemoteVideoProvider.setPauseImage(uri);
+ }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestVideoProvider.java b/testapps/src/com/android/server/telecom/testapps/TestVideoProvider.java
new file mode 100644
index 0000000..d372e46
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestVideoProvider.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2014 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.android.server.telecom.testapps;
+
+import com.android.ex.camera2.blocking.BlockingCameraManager;
+import com.android.ex.camera2.blocking.BlockingCameraManager.BlockingOpenException;
+import com.android.ex.camera2.blocking.BlockingSessionCallback;
+import com.android.server.telecom.testapps.R;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.media.MediaPlayer;
+import android.os.Handler;
+import android.telecom.CameraCapabilities;
+import android.telecom.Connection;
+import android.telecom.VideoProfile;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Size;
+import android.view.Surface;
+
+import java.lang.IllegalArgumentException;
+import java.lang.String;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Implements the VideoCallProvider.
+ */
+public class TestVideoProvider extends Connection.VideoProvider {
+ private CameraCapabilities mCameraCapabilities;
+ private Random random;
+ private Surface mDisplaySurface;
+ private Surface mPreviewSurface;
+ private Context mContext;
+ /** Used to play incoming video during a call. */
+ private MediaPlayer mIncomingMediaPlayer;
+
+ private CameraManager mCameraManager;
+ private CameraDevice mCameraDevice;
+ private CameraCaptureSession mCameraSession;
+ private CameraThread mLooperThread;
+
+ private String mCameraId;
+
+ private static final long SESSION_TIMEOUT_MS = 2000;
+
+ public TestVideoProvider(Context context) {
+ mContext = context;
+ random = new Random();
+ mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+ }
+
+ @Override
+ public void onSetCamera(String cameraId) {
+ log("Set camera to " + cameraId);
+ mCameraId = cameraId;
+
+ stopCamera();
+ // Get the capabilities of the camera
+ setCameraCapabilities(mCameraId);
+ }
+
+ @Override
+ public void onSetPreviewSurface(Surface surface) {
+ log("Set preview surface " + (surface == null ? "unset" : "set"));
+ if (mPreviewSurface != null) {
+ stopCamera();
+ }
+
+ mPreviewSurface = surface;
+
+ if (!TextUtils.isEmpty(mCameraId) && mPreviewSurface != null) {
+ startCamera(mCameraId);
+ }
+ }
+
+ @Override
+ public void onSetDisplaySurface(Surface surface) {
+ log("Set display surface " + (surface == null ? "unset" : "set"));
+ mDisplaySurface = surface;
+
+ if (mDisplaySurface != null) {
+ if (mIncomingMediaPlayer == null) {
+ // For a Rick-Rolling good time use R.raw.test_video
+ mIncomingMediaPlayer = createMediaPlayer(mDisplaySurface, R.raw.test_pattern);
+ }
+ mIncomingMediaPlayer.setSurface(mDisplaySurface);
+ if (!mIncomingMediaPlayer.isPlaying()) {
+ mIncomingMediaPlayer.start();
+ }
+ } else {
+ if (mIncomingMediaPlayer != null) {
+ mIncomingMediaPlayer.stop();
+ mIncomingMediaPlayer.setSurface(null);
+ }
+ }
+ }
+
+ @Override
+ public void onSetDeviceOrientation(int rotation) {
+ log("Set device orientation " + rotation);
+ }
+
+ /**
+ * Sets the zoom value, creating a new CallCameraCapabalities object. If the zoom value is
+ * non-positive, assume that zoom is not supported.
+ */
+ @Override
+ public void onSetZoom(float value) {
+ log("Set zoom to " + value);
+ }
+
+ /**
+ * "Sends" a request with a video call profile. Assumes that this response succeeds and sends
+ * the response back via the CallVideoClient.
+ */
+ @Override
+ public void onSendSessionModifyRequest(VideoProfile requestProfile) {
+ log("Sent session modify request");
+
+ VideoProfile responseProfile = new VideoProfile(
+ requestProfile.getVideoState(), requestProfile.getQuality());
+ receiveSessionModifyResponse(
+ SESSION_MODIFY_REQUEST_SUCCESS,
+ requestProfile,
+ responseProfile);
+ }
+
+ @Override
+ public void onSendSessionModifyResponse(VideoProfile responseProfile) {
+
+ }
+
+ /**
+ * Returns a CallCameraCapabilities object without supporting zoom.
+ */
+ @Override
+ public void onRequestCameraCapabilities() {
+ log("Requested camera capabilities");
+ changeCameraCapabilities(mCameraCapabilities);
+ }
+
+ /**
+ * Randomly reports data usage of value ranging from 10MB to 60MB.
+ */
+ @Override
+ public void onRequestConnectionDataUsage() {
+ log("Requested connection data usage");
+ int dataUsageKb = (10 *1024) + random.nextInt(50 * 1024);
+ changeCallDataUsage(dataUsageKb);
+ }
+
+ /**
+ * We do not have a need to set a paused image.
+ */
+ @Override
+ public void onSetPauseImage(String uri) {
+ // Not implemented.
+ }
+
+ /**
+ * Stop and cleanup the media players used for test video playback.
+ */
+ public void stopAndCleanupMedia() {
+ if (mIncomingMediaPlayer != null) {
+ mIncomingMediaPlayer.setSurface(null);
+ mIncomingMediaPlayer.stop();
+ mIncomingMediaPlayer.release();
+ mIncomingMediaPlayer = null;
+ }
+ }
+
+ private static void log(String msg) {
+ Log.w("TestCallVideoProvider", "[TestCallServiceProvider] " + msg);
+ }
+
+ /**
+ * Creates a media player to play a video resource on a surface.
+ * @param surface The surface.
+ * @param videoResource The video resource.
+ * @return The {@code MediaPlayer}.
+ */
+ private MediaPlayer createMediaPlayer(Surface surface, int videoResource) {
+ MediaPlayer mediaPlayer = MediaPlayer.create(mContext.getApplicationContext(),
+ videoResource);
+ mediaPlayer.setSurface(surface);
+ mediaPlayer.setLooping(true);
+ return mediaPlayer;
+ }
+
+ /**
+ * Starts displaying the camera image on the preview surface.
+ *
+ * @param cameraId
+ */
+ private void startCamera(String cameraId) {
+ stopCamera();
+
+ if (mPreviewSurface == null) {
+ return;
+ }
+
+ // Configure a looper thread.
+ mLooperThread = new CameraThread();
+ Handler mHandler;
+ try {
+ mHandler = mLooperThread.start();
+ } catch (Exception e) {
+ log("Exception: " + e);
+ return;
+ }
+
+ // Get the camera device.
+ try {
+ BlockingCameraManager blockingCameraManager = new BlockingCameraManager(mCameraManager);
+ mCameraDevice = blockingCameraManager.openCamera(cameraId, null /* listener */,
+ mHandler);
+ } catch (CameraAccessException e) {
+ log("CameraAccessException: " + e);
+ return;
+ } catch (BlockingOpenException be) {
+ log("BlockingOpenException: " + be);
+ return;
+ }
+
+ // Create a capture session to get the preview and display it on the surface.
+ List<Surface> surfaces = new ArrayList<Surface>();
+ surfaces.add(mPreviewSurface);
+ CaptureRequest.Builder mCaptureRequest = null;
+ try {
+ BlockingSessionCallback blkSession = new BlockingSessionCallback();
+ mCameraDevice.createCaptureSession(surfaces, blkSession, mHandler);
+ mCaptureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+ mCaptureRequest.addTarget(mPreviewSurface);
+ mCameraSession = blkSession.waitAndGetSession(SESSION_TIMEOUT_MS);
+ } catch (CameraAccessException e) {
+ log("CameraAccessException: " + e);
+ return;
+ }
+
+ // Keep repeating
+ try {
+ mCameraSession.setRepeatingRequest(mCaptureRequest.build(), new CameraCaptureCallback(),
+ mHandler);
+ } catch (CameraAccessException e) {
+ log("CameraAccessException: " + e);
+ return;
+ }
+ }
+
+ /**
+ * Stops the camera and looper thread.
+ */
+ public void stopCamera() {
+ try {
+ if (mCameraDevice != null) {
+ mCameraDevice.close();
+ mCameraDevice = null;
+ }
+ if (mLooperThread != null) {
+ mLooperThread.close();
+ mLooperThread = null;
+ }
+ } catch (Exception e) {
+ log("stopCamera Exception: "+e.toString());
+ }
+ }
+
+ /**
+ * Required listener for camera capture events.
+ */
+ private class CameraCaptureCallback extends CameraCaptureSession.CaptureCallback {
+ @Override
+ public void onCaptureCompleted(CameraCaptureSession camera, CaptureRequest request,
+ TotalCaptureResult result) {
+ }
+
+ @Override
+ public void onCaptureFailed(CameraCaptureSession camera, CaptureRequest request,
+ CaptureFailure failure) {
+ }
+ }
+
+ /**
+ * Uses the camera manager to retrieve the camera capabilities for the chosen camera.
+ *
+ * @param cameraId The camera ID to get the capabilities for.
+ */
+ private void setCameraCapabilities(String cameraId) {
+ CameraManager cameraManager = (CameraManager) mContext.getSystemService(
+ Context.CAMERA_SERVICE);
+
+ CameraCharacteristics c = null;
+ try {
+ c = cameraManager.getCameraCharacteristics(cameraId);
+ } catch (IllegalArgumentException | CameraAccessException e) {
+ // Ignoring camera problems.
+ }
+ if (c != null) {
+ // Get the video size for the camera
+ StreamConfigurationMap map = c.get(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+ Size previewSize = map.getOutputSizes(SurfaceTexture.class)[0];
+
+ mCameraCapabilities = new CameraCapabilities(true, 1.0f, previewSize.getWidth(),
+ previewSize.getHeight());
+ }
+ }
+}