Revert "Revert "Support car-mode UI in telecom.""
This reverts commit 88cda8452124d43c145d54a97477dc5cf8a32a58.
Change-Id: Icfd1ab3ed3af39f62bd828dceb5ff2d4d517cb91
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 32b4294..07b8c2b 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -192,7 +192,8 @@
InCallWakeLockControllerFactory inCallWakeLockControllerFactory,
CallAudioManager.AudioServiceFactory audioServiceFactory,
BluetoothManager bluetoothManager,
- WiredHeadsetManager wiredHeadsetManager) {
+ WiredHeadsetManager wiredHeadsetManager,
+ SystemStateProvider systemStateProvider) {
mContext = context;
mLock = lock;
mContactsAsyncHelper = contactsAsyncHelper;
@@ -238,7 +239,7 @@
mProximitySensorManager = proximitySensorManagerFactory.create(context, this);
mPhoneStateBroadcaster = new PhoneStateBroadcaster(this);
mCallLogManager = new CallLogManager(context, phoneAccountRegistrar);
- mInCallController = new InCallController(context, mLock, this);
+ mInCallController = new InCallController(context, mLock, this, systemStateProvider);
mDtmfLocalTonePlayer = new DtmfLocalTonePlayer(context);
mConnectionServiceRepository =
new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, mLock, this);
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index df19474..26ff625 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -47,10 +47,12 @@
// TODO: Needed for move to system service: import com.android.internal.R;
import com.android.internal.telecom.IInCallService;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.SystemStateProvider.SystemStateListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -136,6 +138,19 @@
}
};
+ private final SystemStateListener mSystemStateListener = new SystemStateListener() {
+ @Override
+ public void onCarModeChanged(boolean isCarMode) {
+ // Do something when the car mode changes.
+ }
+ };
+
+ private static final int IN_CALL_SERVICE_TYPE_INVALID = 0;
+ private static final int IN_CALL_SERVICE_TYPE_DIALER_UI = 1;
+ private static final int IN_CALL_SERVICE_TYPE_SYSTEM_UI = 2;
+ private static final int IN_CALL_SERVICE_TYPE_CAR_MODE_UI = 3;
+ private static final int IN_CALL_SERVICE_TYPE_NON_UI = 4;
+
/**
* Maintains a binding connection to the in-call app(s).
* ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
@@ -161,17 +176,21 @@
private final Context mContext;
private final TelecomSystem.SyncRoot mLock;
private final CallsManager mCallsManager;
+ private final SystemStateProvider mSystemStateProvider;
- public InCallController(
- Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager) {
+ public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
+ SystemStateProvider systemStateProvider) {
mContext = context;
mLock = lock;
mCallsManager = callsManager;
- Resources resources = mContext.getResources();
+ mSystemStateProvider = systemStateProvider;
+ Resources resources = mContext.getResources();
mSystemInCallComponentName = new ComponentName(
resources.getString(R.string.ui_default_package),
resources.getString(R.string.incall_default_class));
+
+ mSystemStateProvider.addListener(mSystemStateListener);
}
@Override
@@ -330,82 +349,80 @@
* @param call The newly added call that triggered the binding to the in-call services.
*/
private void bindToServices(Call call) {
+ ComponentName inCallUIService = null;
+ ComponentName carModeInCallUIService = null;
+ List<ComponentName> nonUIInCallServices = new LinkedList<>();
+
+ // Loop through all the InCallService implementations that exist in the devices;
PackageManager packageManager = mContext.getPackageManager();
Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE);
-
- List<ComponentName> inCallControlServices = new ArrayList<>();
- ComponentName inCallUIService = null;
-
for (ResolveInfo entry :
packageManager.queryIntentServices(serviceIntent, PackageManager.GET_META_DATA)) {
ServiceInfo serviceInfo = entry.serviceInfo;
if (serviceInfo != null) {
- boolean hasServiceBindPermission = serviceInfo.permission != null &&
- serviceInfo.permission.equals(
- Manifest.permission.BIND_INCALL_SERVICE);
- if (!hasServiceBindPermission) {
- Log.w(this, "InCallService does not have BIND_INCALL_SERVICE permission: " +
- serviceInfo.packageName);
- continue;
- }
+ ComponentName componentName =
+ new ComponentName(serviceInfo.packageName, serviceInfo.name);
- boolean hasControlInCallPermission = packageManager.checkPermission(
- Manifest.permission.CONTROL_INCALL_EXPERIENCE,
- serviceInfo.packageName) == PackageManager.PERMISSION_GRANTED;
- boolean isDefaultDialerPackage = Objects.equals(serviceInfo.packageName,
- DefaultDialerManager.getDefaultDialerApplication(mContext));
- if (!hasControlInCallPermission && !isDefaultDialerPackage) {
- Log.w(this, "Service does not have CONTROL_INCALL_EXPERIENCE permission: %s"
- + " and is not system or default dialer.", serviceInfo.packageName);
- continue;
- }
+ switch (getInCallServiceType(entry.serviceInfo, packageManager)) {
+ case IN_CALL_SERVICE_TYPE_DIALER_UI:
+ if (inCallUIService == null ||
+ inCallUIService.compareTo(componentName) > 0) {
+ inCallUIService = componentName;
+ }
+ break;
- boolean isUIService = serviceInfo.metaData != null &&
- serviceInfo.metaData.getBoolean(
- TelecomManager.METADATA_IN_CALL_SERVICE_UI, false);
- ComponentName componentName = new ComponentName(serviceInfo.packageName,
- serviceInfo.name);
- if (isUIService) {
- // For the main UI service, we always prefer the default dialer.
- if (isDefaultDialerPackage) {
- inCallUIService = componentName;
- Log.i(this, "Found default-dialer's In-Call UI: %s", componentName);
- }
- } else {
- // for non-UI services that have passed our checks, add them to the list of
- // service to bind to.
- inCallControlServices.add(componentName);
- }
+ case IN_CALL_SERVICE_TYPE_SYSTEM_UI:
+ // skip, will be added manually
+ break;
+ case IN_CALL_SERVICE_TYPE_CAR_MODE_UI:
+ if (carModeInCallUIService == null ||
+ carModeInCallUIService.compareTo(componentName) > 0) {
+ carModeInCallUIService = componentName;
+ }
+ break;
+
+ case IN_CALL_SERVICE_TYPE_NON_UI:
+ nonUIInCallServices.add(componentName);
+ break;
+
+ case IN_CALL_SERVICE_TYPE_INVALID:
+ break;
+
+ default:
+ Log.w(this, "unexpected in-call service type");
+ break;
+ }
}
}
- // Attempt to bind to the default-dialer InCallService first.
- if (inCallUIService != null) {
- // skip default dialer if we have an emergency call or if it failed binding.
- if (mCallsManager.hasEmergencyCall()) {
- Log.i(this, "Skipping default-dialer because of emergency call");
- inCallUIService = null;
- } else if (!bindToInCallService(inCallUIService, call, "def-dialer")) {
- Log.event(call, Log.Events.ERROR_LOG,
- "InCallService UI failed binding: " + inCallUIService);
- inCallUIService = null;
- }
- }
+ Log.i(this, "Car mode InCallService: %s", carModeInCallUIService);
+ Log.i(this, "Dialer InCallService: %s", inCallUIService);
- if (inCallUIService == null) {
- // We failed to connect to the default-dialer service, or none was provided. Switch to
- // the system built-in InCallService UI.
- inCallUIService = mSystemInCallComponentName;
- if (!bindToInCallService(inCallUIService, call, "system")) {
- Log.event(call, Log.Events.ERROR_LOG,
- "InCallService system UI failed binding: " + inCallUIService);
- }
+ // Adding the in-call services in order:
+ // (1) The carmode in-call if carmode is on.
+ // (2) The default-dialer in-call if not an emergency call
+ // (3) The system-provided in-call
+ List<ComponentName> orderedInCallUIServices = new LinkedList<>();
+ if (shouldUseCarModeUI() && carModeInCallUIService != null) {
+ orderedInCallUIServices.add(carModeInCallUIService);
+ }
+ if (!mCallsManager.hasEmergencyCall() && inCallUIService != null) {
+ orderedInCallUIServices.add(inCallUIService);
+ }
+ orderedInCallUIServices.add(mSystemInCallComponentName);
+
+ // TODO: Need to implement the fall-back logic in case the main UI in-call service rejects
+ // the binding request.
+ ComponentName inCallUIServiceToBind = orderedInCallUIServices.get(0);
+ if (!bindToInCallService(inCallUIServiceToBind, call, "ui")) {
+ Log.event(call, Log.Events.ERROR_LOG,
+ "InCallService system UI failed binding: " + inCallUIService);
}
mInCallUIComponentName = inCallUIService;
// Bind to the control InCallServices
- for (ComponentName componentName : inCallControlServices) {
+ for (ComponentName componentName : nonUIInCallServices) {
bindToInCallService(componentName, call, "control");
}
}
@@ -445,6 +462,68 @@
return false;
}
+ private boolean shouldUseCarModeUI() {
+ return mSystemStateProvider.isCarMode();
+ }
+
+ /**
+ * Returns the type of InCallService described by the specified serviceInfo.
+ */
+ private int getInCallServiceType(ServiceInfo serviceInfo, PackageManager packageManager) {
+ // Verify that the InCallService requires the BIND_INCALL_SERVICE permission which
+ // enforces that only Telecom can bind to it.
+ boolean hasServiceBindPermission = serviceInfo.permission != null &&
+ serviceInfo.permission.equals(
+ Manifest.permission.BIND_INCALL_SERVICE);
+ if (!hasServiceBindPermission) {
+ Log.w(this, "InCallService does not require BIND_INCALL_SERVICE permission: " +
+ serviceInfo.packageName);
+ return IN_CALL_SERVICE_TYPE_INVALID;
+ }
+
+ if (mSystemInCallComponentName.getPackageName().equals(serviceInfo.packageName) &&
+ mSystemInCallComponentName.getClassName().equals(serviceInfo.name)) {
+ return IN_CALL_SERVICE_TYPE_SYSTEM_UI;
+ }
+
+ // Check to see if the service is a car-mode UI type by checking that it has the
+ // CONTROL_INCALL_EXPERIENCE (to verify it is a system app) and that it has the
+ // car-mode UI metadata.
+ boolean hasControlInCallPermission = packageManager.checkPermission(
+ Manifest.permission.CONTROL_INCALL_EXPERIENCE,
+ serviceInfo.packageName) == PackageManager.PERMISSION_GRANTED;
+ boolean isCarModeUIService = serviceInfo.metaData != null &&
+ serviceInfo.metaData.getBoolean(
+ TelecomManager.METADATA_IN_CALL_SERVICE_CAR_MODE_UI, false) &&
+ hasControlInCallPermission;
+ if (isCarModeUIService) {
+ return IN_CALL_SERVICE_TYPE_CAR_MODE_UI;
+ }
+
+
+ // Check to see that it is the default dialer package
+ boolean isDefaultDialerPackage = Objects.equals(serviceInfo.packageName,
+ DefaultDialerManager.getDefaultDialerApplication(mContext));
+ boolean isUIService = serviceInfo.metaData != null &&
+ serviceInfo.metaData.getBoolean(
+ TelecomManager.METADATA_IN_CALL_SERVICE_UI, false);
+ if (isDefaultDialerPackage && isUIService) {
+ return IN_CALL_SERVICE_TYPE_DIALER_UI;
+ }
+
+ // Also allow any in-call service that has the control-experience permission (to ensure
+ // that it is a system app) and doesn't claim to show any UI.
+ if (hasControlInCallPermission && !isUIService) {
+ return IN_CALL_SERVICE_TYPE_NON_UI;
+ }
+
+ // Anything else that remains, we will not bind to.
+ Log.i(this, "Skipping binding to %s:%s, control: %b, car-mode: %b, ui: %b",
+ serviceInfo.packageName, serviceInfo.name, hasControlInCallPermission,
+ isCarModeUIService, isUIService);
+ return IN_CALL_SERVICE_TYPE_INVALID;
+ }
+
private void adjustServiceBindingsForEmergency() {
if (!Objects.equals(mInCallUIComponentName, mSystemInCallComponentName)) {
// The connected UI is not the system UI, so lets check if we should switch them
diff --git a/src/com/android/server/telecom/SystemStateProvider.java b/src/com/android/server/telecom/SystemStateProvider.java
new file mode 100644
index 0000000..0b636cf
--- /dev/null
+++ b/src/com/android/server/telecom/SystemStateProvider.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2015 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;
+
+import android.app.UiModeManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * Provides various system states to the rest of the telecom codebase. So far, that's only car-mode.
+ */
+public class SystemStateProvider {
+
+ public static interface SystemStateListener {
+ public void onCarModeChanged(boolean isCarMode);
+ }
+
+ private final Context mContext;
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.startSession("SSP.oR");
+ try {
+ String action = intent.getAction();
+ if (UiModeManager.ACTION_ENTER_CAR_MODE.equals(action)) {
+ onEnterCarMode();
+ } else if (UiModeManager.ACTION_EXIT_CAR_MODE.equals(action)) {
+ onExitCarMode();
+ } else {
+ Log.w(this, "Unexpected intent received: %s", intent.getAction());
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+ };
+
+ private Set<SystemStateListener> mListeners = new CopyOnWriteArraySet<>();
+ private boolean mIsCarMode;
+
+ public SystemStateProvider(Context context) {
+ mContext = context;
+
+ IntentFilter intentFilter = new IntentFilter(UiModeManager.ACTION_ENTER_CAR_MODE);
+ intentFilter.addAction(UiModeManager.ACTION_EXIT_CAR_MODE);
+ mContext.registerReceiver(mBroadcastReceiver, intentFilter);
+ Log.i(this, "Registering car mode receiver: %s", intentFilter);
+
+ mIsCarMode = getSystemCarMode();
+ }
+
+ public void addListener(SystemStateListener listener) {
+ if (listener != null) {
+ mListeners.add(listener);
+ }
+ }
+
+ public boolean removeListener(SystemStateListener listener) {
+ return mListeners.remove(listener);
+ }
+
+ public boolean isCarMode() {
+ return mIsCarMode;
+ }
+
+ private void onEnterCarMode() {
+ if (!mIsCarMode) {
+ Log.i(this, "Entering carmode");
+ mIsCarMode = true;
+ notifyCarMode();
+ }
+ }
+
+ private void onExitCarMode() {
+ if (mIsCarMode) {
+ Log.i(this, "Exiting carmode");
+ mIsCarMode = false;
+ notifyCarMode();
+ }
+ }
+
+ private void notifyCarMode() {
+ for (SystemStateListener listener : mListeners) {
+ listener.onCarModeChanged(mIsCarMode);
+ }
+ }
+
+ /**
+ * Checks the system for the current car mode.
+ *
+ * @return True if in car mode, false otherwise.
+ */
+ private boolean getSystemCarMode() {
+ UiModeManager uiModeManager =
+ (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
+
+ if (uiModeManager != null) {
+ return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR;
+ }
+
+ return false;
+ }
+}
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index d0528e8..e016ebd 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -156,6 +156,7 @@
});
BluetoothManager bluetoothManager = new BluetoothManager(mContext);
WiredHeadsetManager wiredHeadsetManager = new WiredHeadsetManager(mContext);
+ SystemStateProvider systemStateProvider = new SystemStateProvider(mContext);
mMissedCallNotifier = missedCallNotifierImplFactory
.makeMissedCallNotifierImpl(mContext, mPhoneAccountRegistrar);
@@ -172,7 +173,8 @@
inCallWakeLockControllerFactory,
audioServiceFactory,
bluetoothManager,
- wiredHeadsetManager);
+ wiredHeadsetManager,
+ systemStateProvider);
mRespondViaSmsManager = new RespondViaSmsManager(mCallsManager, mLock);
mCallsManager.setRespondViaSmsManager(mRespondViaSmsManager);
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
index 0672b6d..e645123 100644
--- a/testapps/AndroidManifest.xml
+++ b/testapps/AndroidManifest.xml
@@ -54,6 +54,7 @@
<service android:name="com.android.server.telecom.testapps.TestInCallServiceImpl"
android:process="com.android.server.telecom.testapps.TestInCallService"
android:permission="android.permission.BIND_INCALL_SERVICE" >
+ <meta-data android:name="android.telecom.IN_CALL_SERVICE_CAR_MODE_UI" android:value="true" />
<intent-filter>
<action android:name="android.telecom.InCallService"/>
</intent-filter>
@@ -68,6 +69,17 @@
</intent-filter>
</receiver>
+ <activity android:name="com.android.server.telecom.testapps.TestInCallUI"
+ android:process="com.android.server.telecom.testapps.TestInCallService"
+ android:label="@string/inCallUiAppLabel"
+ android:launchMode="singleInstance">
+ <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>
+
<activity android:name="com.android.server.telecom.testapps.TestCallActivity"
android:theme="@android:style/Theme.NoDisplay"
android:label="@string/testCallActivityLabel">
diff --git a/testapps/res/layout/call_list_item.xml b/testapps/res/layout/call_list_item.xml
new file mode 100644
index 0000000..c9f2ff7
--- /dev/null
+++ b/testapps/res/layout/call_list_item.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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="wrap_content"
+ android:orientation="vertical" >
+ <TextView
+ android:id="@+id/phoneNumber"
+ android:layout_gravity="left"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="25dp"
+ android:text="TextView" />
+ <TextView
+ android:id="@+id/callState"
+ android:layout_gravity="left"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="25dp"
+ android:text="TextView" />
+ <TextView
+ android:id="@+id/duration"
+ android:layout_gravity="right"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="25dp"
+ android:text="TextView" />
+</LinearLayout>
diff --git a/testapps/res/layout/incall_screen.xml b/testapps/res/layout/incall_screen.xml
new file mode 100644
index 0000000..6a891e7
--- /dev/null
+++ b/testapps/res/layout/incall_screen.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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" >
+ <ListView
+ android:id="@+id/callListView"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:divider="#FFCC00"
+ android:dividerHeight="4px">
+ </ListView>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+ <Button
+ android:id="@+id/end_call_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/endCallButton" />
+ <Button
+ android:id="@+id/mute_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/muteButton" />
+ <Button
+ android:id="@+id/hold_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/holdButton" >
+ </Button>
+ </LinearLayout>
+</LinearLayout>
diff --git a/testapps/res/values/donottranslate_strings.xml b/testapps/res/values/donottranslate_strings.xml
index 43c302d..599d5cc 100644
--- a/testapps/res/values/donottranslate_strings.xml
+++ b/testapps/res/values/donottranslate_strings.xml
@@ -37,4 +37,12 @@
<!-- String for button in TestDialerActivity that tries to exercise the
TelecomManager.cancelMissedCallNotifications() functionality -->
<string name="cancelMissedButton">Cancel missed calls</string>
+
+ <string name="endCallButton">End Call</string>
+
+ <string name="muteButton">Mute</string>
+
+ <string name="holdButton">Hold</string>
+
+ <string name="inCallUiAppLabel">Test InCall UI</string>
</resources>
diff --git a/testapps/src/com/android/server/telecom/testapps/CallListAdapter.java b/testapps/src/com/android/server/telecom/testapps/CallListAdapter.java
new file mode 100644
index 0000000..bea0e63
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/CallListAdapter.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2015 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.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.telecom.Call;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+public class CallListAdapter extends BaseAdapter {
+ private static final String TAG = "CallListAdapter";
+
+ private final TestCallList.Listener mListener = new TestCallList.Listener() {
+ @Override
+ public void onCallAdded(Call call) {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ notifyDataSetChanged();
+ if (mCallList.size() == 0) {
+ mCallList.removeListener(this);
+ }
+ }
+ };
+
+ private final LayoutInflater mLayoutInflater;
+ private final TestCallList mCallList;
+ private final Handler mHandler = new Handler();
+ private final Runnable mSecondsRunnable = new Runnable() {
+ @Override
+ public void run() {
+ notifyDataSetChanged();
+ if (mCallList.size() > 0) {
+ mHandler.postDelayed(this, 1000);
+ }
+ }
+ };
+
+ public CallListAdapter(Context context) {
+ mLayoutInflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mCallList = TestCallList.getInstance();
+ mCallList.addListener(mListener);
+ mHandler.postDelayed(mSecondsRunnable, 1000);
+ }
+
+
+ @Override
+ public int getCount() {
+ Log.i(TAG, "size reporting: " + mCallList.size());
+ return mCallList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return position;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ Log.i(TAG, "getView: " + position);
+ if (convertView == null) {
+ convertView = mLayoutInflater.inflate(R.layout.call_list_item, parent, false);
+ }
+
+ TextView phoneNumber = (TextView) convertView.findViewById(R.id.phoneNumber);
+ TextView duration = (TextView) convertView.findViewById(R.id.duration);
+ TextView state = (TextView) convertView.findViewById(R.id.callState);
+
+ Call call = mCallList.getCall(position);
+ Uri handle = call.getDetails().getHandle();
+ phoneNumber.setText(handle == null ? "No number" : handle.getSchemeSpecificPart());
+
+ long durationMs = System.currentTimeMillis() - call.getDetails().getConnectTimeMillis();
+ duration.setText((durationMs / 1000) + " secs");
+
+ state.setText(getStateString(call));
+
+ Log.i(TAG, "Call found: " + handle.getSchemeSpecificPart() + ", " + durationMs);
+
+ return convertView;
+ }
+
+ private static String getStateString(Call call) {
+ switch (call.getState()) {
+ case Call.STATE_ACTIVE:
+ return "active";
+ case Call.STATE_CONNECTING:
+ return "connecting";
+ case Call.STATE_DIALING:
+ return "dialing";
+ case Call.STATE_DISCONNECTED:
+ return "disconnected";
+ case Call.STATE_DISCONNECTING:
+ return "disconnecting";
+ case Call.STATE_HOLDING:
+ return "on hold";
+ case Call.STATE_NEW:
+ return "new";
+ case Call.STATE_RINGING:
+ return "ringing";
+ case Call.STATE_SELECT_PHONE_ACCOUNT:
+ return "select phone account";
+ default:
+ return "unknown";
+ }
+ }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestCallList.java b/testapps/src/com/android/server/telecom/testapps/TestCallList.java
index a16c4e2..704c83d 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestCallList.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestCallList.java
@@ -24,6 +24,8 @@
import android.util.ArraySet;
import android.util.Log;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -31,6 +33,12 @@
* Maintains a list of calls received via the {@link TestInCallServiceImpl}.
*/
public class TestCallList extends Call.Listener {
+
+ public static abstract class Listener {
+ public void onCallAdded(Call call) {}
+ public void onCallRemoved(Call call) {}
+ }
+
private static final TestCallList INSTANCE = new TestCallList();
private static final String TAG = "TestCallList";
@@ -85,9 +93,10 @@
}
// The calls the call list knows about.
- private Set<Call> mCalls = new ArraySet<Call>();
+ private List<Call> mCalls = new LinkedList<Call>();
private Map<Call, TestVideoCallListener> mVideoCallListeners =
new ArrayMap<Call, TestVideoCallListener>();
+ private Set<Listener> mListeners = new ArraySet<Listener>();
/**
* Singleton accessor.
@@ -96,14 +105,32 @@
return INSTANCE;
}
+ public void addListener(Listener listener) {
+ if (listener != null) {
+ mListeners.add(listener);
+ }
+ }
+
+ public boolean removeListener(Listener listener) {
+ return mListeners.remove(listener);
+ }
+
+ public Call getCall(int position) {
+ return mCalls.get(position);
+ }
+
public void addCall(Call call) {
if (mCalls.contains(call)) {
Log.e(TAG, "addCall: Call already added.");
return;
}
- Log.v(TAG, "addCall: " + call + " " + System.identityHashCode(this));
+ Log.i(TAG, "addCall: " + call + " " + System.identityHashCode(this));
mCalls.add(call);
call.addListener(this);
+
+ for (Listener l : mListeners) {
+ l.onCallAdded(call);
+ }
}
public void removeCall(Call call) {
@@ -111,9 +138,13 @@
Log.e(TAG, "removeCall: Call cannot be removed -- doesn't exist.");
return;
}
- Log.v(TAG, "removeCall: " + call);
+ Log.i(TAG, "removeCall: " + call);
mCalls.remove(call);
call.removeListener(this);
+
+ for (Listener l : mListeners) {
+ l.onCallRemoved(call);
+ }
}
public void clearCalls() {
@@ -126,6 +157,10 @@
mVideoCallListeners.clear();
}
+ public int size() {
+ return mCalls.size();
+ }
+
/**
* For any video calls tracked, sends an upgrade to video request.
*/
diff --git a/testapps/src/com/android/server/telecom/testapps/TestInCallServiceImpl.java b/testapps/src/com/android/server/telecom/testapps/TestInCallServiceImpl.java
index 68bbac9..03ca3d0 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestInCallServiceImpl.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestInCallServiceImpl.java
@@ -16,6 +16,8 @@
package com.android.server.telecom.testapps;
+import android.content.Context;
+import android.content.Intent;
import android.telecom.Call;
import android.telecom.InCallService;
import android.telecom.Phone;
@@ -37,7 +39,12 @@
@Override
public void onCallAdded(Phone phone, Call call) {
Log.i(TAG, "onCallAdded: " + call.toString());
- TestCallList.getInstance().addCall(call);
+ TestCallList callList = TestCallList.getInstance();
+ callList.addCall(call);
+
+ if (callList.size() == 1) {
+ startInCallUI();
+ }
}
@Override
@@ -62,4 +69,11 @@
mPhone = null;
TestCallList.getInstance().clearCalls();
}
+
+ private void startInCallUI() {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setClass(this, TestInCallUI.class);
+ startActivity(intent);
+ }
}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
new file mode 100644
index 0000000..ce53709
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2015 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.graphics.Color;
+import android.os.Bundle;
+import android.telecom.Call;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ListView;
+
+public class TestInCallUI extends Activity {
+
+ private ListView mListView;
+ private TestCallList mCallList;
+
+ /** ${inheritDoc} */
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.incall_screen);
+
+ mListView = (ListView) findViewById(R.id.callListView);
+ mListView.setAdapter(new CallListAdapter(this));
+ mListView.setVisibility(View.VISIBLE);
+
+ mCallList = TestCallList.getInstance();
+ mCallList.addListener(new TestCallList.Listener() {
+ @Override
+ public void onCallRemoved(Call call) {
+ if (mCallList.size() == 0) {
+ Log.i("Santos", "Ending the incall UI");
+ finish();
+ }
+ }
+ });
+
+ View endCallButton = findViewById(R.id.end_call_button);
+ View holdButton = findViewById(R.id.hold_button);
+ View muteButton = findViewById(R.id.mute_button);
+
+ endCallButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Call call = mCallList.getCall(0);
+ if (call != null) {
+ call.disconnect();
+ }
+ }
+ });
+ holdButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Call call = mCallList.getCall(0);
+ if (call != null) {
+ if (call.getState() == Call.STATE_HOLDING) {
+ call.unhold();
+ } else {
+ call.hold();
+ }
+ }
+ }
+ });
+ muteButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Call call = mCallList.getCall(0);
+ if (call != null) {
+ }
+ }
+ });
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ }
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 4576690..f84a545 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -47,8 +47,8 @@
adb shell am instrument -w com.android.server.telecom.tests/android.test.InstrumentationTestRunner
To run a single test case:
- adb shell am instrument -w com.android.server.telecom.tests/android.test.InstrumentationTestRunner
- -e com.android.server.telecom.tests.unit.FooUnitTest
+ adb shell am instrument -w -e class com.android.server.telecom.tests.unit.FooUnitTest \
+ com.android.server.telecom.tests/android.test.InstrumentationTestRunner
-->
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.android.server.telecom.tests"
diff --git a/tests/src/com/android/server/telecom/tests/SystemStateProviderTest.java b/tests/src/com/android/server/telecom/tests/SystemStateProviderTest.java
new file mode 100644
index 0000000..02e7ecf
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/SystemStateProviderTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2015 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.tests;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.UiModeManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+
+import com.android.server.telecom.SystemStateProvider;
+import com.android.server.telecom.SystemStateProvider.SystemStateListener;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for SystemStateProvider
+ */
+public class SystemStateProviderTest extends TelecomTestCase {
+
+ SystemStateProvider mSystemStateProvider;
+
+ @Mock Context mContext;
+ @Mock SystemStateListener mSystemStateListener;
+ @Mock UiModeManager mUiModeManager;
+ @Mock Intent mIntentEnter;
+ @Mock Intent mIntentExit;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void testListeners() throws Exception {
+ SystemStateProvider systemStateProvider = new SystemStateProvider(mContext);
+
+ assertFalse(systemStateProvider.removeListener(mSystemStateListener));
+ systemStateProvider.addListener(mSystemStateListener);
+ assertTrue(systemStateProvider.removeListener(mSystemStateListener));
+ assertFalse(systemStateProvider.removeListener(mSystemStateListener));
+ }
+
+ public void testQuerySystemForCarMode_True() {
+ when(mContext.getSystemService(Context.UI_MODE_SERVICE)).thenReturn(mUiModeManager);
+ when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+ assertTrue(new SystemStateProvider(mContext).isCarMode());
+ }
+
+ public void testQuerySystemForCarMode_False() {
+ when(mContext.getSystemService(Context.UI_MODE_SERVICE)).thenReturn(mUiModeManager);
+ when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_NORMAL);
+ assertFalse(new SystemStateProvider(mContext).isCarMode());
+ }
+
+ public void testReceiverAndIntentFilter() {
+ ArgumentCaptor<IntentFilter> intentFilter = ArgumentCaptor.forClass(IntentFilter.class);
+ new SystemStateProvider(mContext);
+ verify(mContext).registerReceiver(any(BroadcastReceiver.class), intentFilter.capture());
+
+ assertEquals(2, intentFilter.getValue().countActions());
+ assertEquals(UiModeManager.ACTION_ENTER_CAR_MODE, intentFilter.getValue().getAction(0));
+ assertEquals(UiModeManager.ACTION_EXIT_CAR_MODE, intentFilter.getValue().getAction(1));
+ }
+
+ public void testOnEnterExitCarMode() {
+ ArgumentCaptor<BroadcastReceiver> receiver =
+ ArgumentCaptor.forClass(BroadcastReceiver.class);
+ new SystemStateProvider(mContext).addListener(mSystemStateListener);
+
+ verify(mContext).registerReceiver(receiver.capture(), any(IntentFilter.class));
+
+ when(mIntentEnter.getAction()).thenReturn(UiModeManager.ACTION_ENTER_CAR_MODE);
+ receiver.getValue().onReceive(mContext, mIntentEnter);
+ verify(mSystemStateListener).onCarModeChanged(true);
+
+ when(mIntentExit.getAction()).thenReturn(UiModeManager.ACTION_EXIT_CAR_MODE);
+ receiver.getValue().onReceive(mContext, mIntentExit);
+ verify(mSystemStateListener).onCarModeChanged(false);
+
+ receiver.getValue().onReceive(mContext, new Intent("invalid action"));
+ }
+}