diff options
Diffstat (limited to 'java/com')
20 files changed, 580 insertions, 102 deletions
diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java index 679901e9f..016bce322 100644 --- a/java/com/android/dialer/app/calllog/CallLogAdapter.java +++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java @@ -75,6 +75,7 @@ import com.android.dialer.compat.android.provider.VoicemailCompat; import com.android.dialer.configprovider.ConfigProviderBindings; import com.android.dialer.duo.Duo; import com.android.dialer.duo.DuoComponent; +import com.android.dialer.duo.DuoConstants; import com.android.dialer.duo.DuoListener; import com.android.dialer.enrichedcall.EnrichedCallCapabilities; import com.android.dialer.enrichedcall.EnrichedCallComponent; @@ -399,11 +400,7 @@ public class CallLogAdapter extends GroupingListAdapter if (intentProvider == null) { return false; } - String packageName = DuoComponent.get(mActivity).getDuo().getPackageName(); - if (packageName == null) { - return false; - } - return packageName.equals(intentProvider.getIntent(mActivity).getPackage()); + return DuoConstants.PACKAGE_NAME.equals(intentProvider.getIntent(mActivity).getPackage()); } }; @@ -697,7 +694,7 @@ public class CallLogAdapter extends GroupingListAdapter @Override protected void addGroups(Cursor cursor) { - mCallLogGroupBuilder.addGroups(cursor, mActivity); + mCallLogGroupBuilder.addGroups(cursor); } @Override @@ -981,11 +978,9 @@ public class CallLogAdapter extends GroupingListAdapter .setFeatures(cursor.getInt(CallLogQuery.FEATURES)); String phoneAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME); - if (getDuo().getPhoneAccountComponentName() != null - && getDuo() - .getPhoneAccountComponentName() - .flattenToString() - .equals(phoneAccountComponentName)) { + if (DuoConstants.PHONE_ACCOUNT_COMPONENT_NAME + .flattenToString() + .equals(phoneAccountComponentName)) { entry.setIsDuoCall(true); } diff --git a/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java b/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java index 57a8be730..513c8aa59 100644 --- a/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java +++ b/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java @@ -16,7 +16,6 @@ package com.android.dialer.app.calllog; -import android.content.Context; import android.database.Cursor; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; @@ -74,7 +73,7 @@ public class CallLogGroupBuilder { * * @see GroupingListAdapter#addGroups(Cursor) */ - public void addGroups(Cursor cursor, Context context) { + public void addGroups(Cursor cursor) { final int count = cursor.getCount(); if (count == 0) { return; @@ -99,7 +98,7 @@ public class CallLogGroupBuilder { int groupFeatures = cursor.getInt(CallLogQuery.FEATURES); int groupCallbackAction = CallbackActionHelper.getCallbackAction( - groupNumber, groupFeatures, groupAccountComponentName, context); + groupNumber, groupFeatures, groupAccountComponentName); mGroupCreator.setCallbackAction(firstRowId, groupCallbackAction); // Instantiate other group values to those of the first call in the cursor. @@ -134,7 +133,7 @@ public class CallLogGroupBuilder { accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME); accountId = cursor.getString(CallLogQuery.ACCOUNT_ID); callbackAction = - CallbackActionHelper.getCallbackAction(number, features, accountComponentName, context); + CallbackActionHelper.getCallbackAction(number, features, accountComponentName); final boolean isSameNumber = equalNumbers(groupNumber, number); final boolean isSamePostDialDigits = groupPostDialDigits.equals(numberPostDialDigits); diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java index f0852bdb5..d5dfb06dc 100644 --- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java +++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java @@ -27,7 +27,6 @@ import android.provider.CallLog; import android.provider.CallLog.Calls; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.support.annotation.IntDef; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.CardView; @@ -75,8 +74,7 @@ import com.android.dialer.constants.ActivityRequestCodes; import com.android.dialer.contactphoto.ContactPhotoManager; import com.android.dialer.dialercontact.DialerContact; import com.android.dialer.dialercontact.SimDetails; -import com.android.dialer.duo.Duo; -import com.android.dialer.duo.DuoComponent; +import com.android.dialer.duo.DuoConstants; import com.android.dialer.lettertile.LetterTileDrawable; import com.android.dialer.lettertile.LetterTileDrawable.ContactType; import com.android.dialer.logging.ContactSource; @@ -746,7 +744,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder private boolean showDuoPrimaryButton() { return accountHandle != null - && accountHandle.getComponentName().equals(getDuo().getPhoneAccountComponentName()) + && accountHandle.getComponentName().equals(DuoConstants.PHONE_ACCOUNT_COMPONENT_NAME) && duoReady; } @@ -961,7 +959,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder // We check to see if we are starting a Duo intent. The reason is Duo // intents need to be started using startActivityForResult instead of the usual startActivity String packageName = intent.getPackage(); - if (packageName != null && packageName.equals(getDuo().getPackageName())) { + if (DuoConstants.PACKAGE_NAME.equals(packageName)) { Logger.get(mContext) .logImpression(DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FROM_CALL_LOG); if (isNonContactEntry(info)) { @@ -1125,11 +1123,6 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder return callDetailsEntries; } - @NonNull - private Duo getDuo() { - return DuoComponent.get(mContext).getDuo(); - } - @Override public void onCreateContextMenu( final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { diff --git a/java/com/android/dialer/app/calllog/MissedCallNotifier.java b/java/com/android/dialer/app/calllog/MissedCallNotifier.java index f50751e2b..fff68d4c4 100644 --- a/java/com/android/dialer/app/calllog/MissedCallNotifier.java +++ b/java/com/android/dialer/app/calllog/MissedCallNotifier.java @@ -53,6 +53,7 @@ import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerExecutor.Worker; import com.android.dialer.compat.android.provider.VoicemailCompat; +import com.android.dialer.duo.DuoConstants; import com.android.dialer.enrichedcall.FuzzyPhoneNumberMatcher; import com.android.dialer.notification.DialerNotificationManager; import com.android.dialer.notification.NotificationChannelId; @@ -253,6 +254,7 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { if (newCalls == null) { return; } + TelecomManager telecomManager = context.getSystemService(TelecomManager.class); Iterator<NewCall> iterator = newCalls.iterator(); while (iterator.hasNext()) { @@ -269,6 +271,10 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { if (phoneAccount == null) { continue; } + if (DuoConstants.PHONE_ACCOUNT_HANDLE.equals(phoneAccountHandle)) { + iterator.remove(); + continue; + } if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) { LogUtil.i( "MissedCallNotifier.removeSelfManagedCalls", diff --git a/java/com/android/dialer/calllogutils/CallbackActionHelper.java b/java/com/android/dialer/calllogutils/CallbackActionHelper.java index 304994305..1e219f144 100644 --- a/java/com/android/dialer/calllogutils/CallbackActionHelper.java +++ b/java/com/android/dialer/calllogutils/CallbackActionHelper.java @@ -16,12 +16,10 @@ package com.android.dialer.calllogutils; -import android.content.Context; import android.provider.CallLog.Calls; import android.support.annotation.IntDef; import android.text.TextUtils; -import com.android.dialer.duo.Duo; -import com.android.dialer.duo.DuoComponent; +import com.android.dialer.duo.DuoConstants; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -45,12 +43,11 @@ public class CallbackActionHelper { * @param features Value of features in column {@link android.provider.CallLog.Calls#FEATURES}. * @param phoneAccountComponentName Account name in column {@link * android.provider.CallLog.Calls#PHONE_ACCOUNT_COMPONENT_NAME}. - * @param context The context in which the method is called. * @return One of the values in {@link CallbackAction} */ public static @CallbackAction int getCallbackAction( - String number, int features, String phoneAccountComponentName, Context context) { - return getCallbackAction(number, features, isDuoCall(phoneAccountComponentName, context)); + String number, int features, String phoneAccountComponentName) { + return getCallbackAction(number, features, isDuoCall(phoneAccountComponentName)); } /** @@ -78,12 +75,9 @@ public class CallbackActionHelper { return CallbackAction.VOICE; } - private static boolean isDuoCall(String phoneAccountComponentName, Context context) { - Duo lightBringer = DuoComponent.get(context).getDuo(); - return lightBringer.getPhoneAccountComponentName() != null - && lightBringer - .getPhoneAccountComponentName() - .flattenToString() - .equals(phoneAccountComponentName); + private static boolean isDuoCall(String phoneAccountComponentName) { + return DuoConstants.PHONE_ACCOUNT_COMPONENT_NAME + .flattenToString() + .equals(phoneAccountComponentName); } } diff --git a/java/com/android/dialer/duo/Duo.java b/java/com/android/dialer/duo/Duo.java index ec07ad4b4..839c1d3a8 100644 --- a/java/com/android/dialer/duo/Duo.java +++ b/java/com/android/dialer/duo/Duo.java @@ -16,7 +16,6 @@ package com.android.dialer.duo; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.support.annotation.MainThread; @@ -24,7 +23,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.telecom.Call; -import android.telecom.PhoneAccountHandle; import com.google.auto.value.AutoValue; import com.google.common.base.Optional; import java.util.List; @@ -55,18 +53,6 @@ public interface Duo { @MainThread void unregisterListener(@NonNull DuoListener listener); - @Nullable - @MainThread - ComponentName getPhoneAccountComponentName(); - - @Nullable - @MainThread - PhoneAccountHandle getPhoneAccountHandle(); - - @Nullable - @MainThread - String getPackageName(); - @StringRes @MainThread int getOutgoingCallTypeText(); diff --git a/java/com/android/dialer/duo/DuoConstants.java b/java/com/android/dialer/duo/DuoConstants.java new file mode 100644 index 000000000..50254ee13 --- /dev/null +++ b/java/com/android/dialer/duo/DuoConstants.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 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.dialer.duo; + +import android.content.ComponentName; +import android.telecom.PhoneAccountHandle; + +/** Constants to reference the Duo application. */ +public final class DuoConstants { + public static final String PACKAGE_NAME = "com.google.android.apps.tachyon"; + + public static final String CONNECTION_SERVICE = + "com.google.android.apps.tachyon.telecom.TachyonTelecomConnectionService"; + + public static final String PHONE_ACCOUNT_ID = "0"; + + public static final ComponentName PHONE_ACCOUNT_COMPONENT_NAME = + new ComponentName(PACKAGE_NAME, CONNECTION_SERVICE); + + public static final PhoneAccountHandle PHONE_ACCOUNT_HANDLE = + new PhoneAccountHandle(PHONE_ACCOUNT_COMPONENT_NAME, PHONE_ACCOUNT_ID); + + private DuoConstants() {} +} diff --git a/java/com/android/dialer/duo/stub/DuoStub.java b/java/com/android/dialer/duo/stub/DuoStub.java index 99f03adfd..82b9c79e3 100644 --- a/java/com/android/dialer/duo/stub/DuoStub.java +++ b/java/com/android/dialer/duo/stub/DuoStub.java @@ -16,7 +16,6 @@ package com.android.dialer.duo.stub; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.support.annotation.MainThread; @@ -24,7 +23,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.telecom.Call; -import android.telecom.PhoneAccountHandle; import com.android.dialer.common.Assert; import com.android.dialer.duo.Duo; import com.android.dialer.duo.DuoListener; @@ -95,24 +93,6 @@ public class DuoStub implements Duo { Assert.isNotNull(listener); } - @Nullable - @Override - public ComponentName getPhoneAccountComponentName() { - return null; - } - - @Nullable - @Override - public PhoneAccountHandle getPhoneAccountHandle() { - return null; - } - - @Nullable - @Override - public String getPackageName() { - return null; - } - @StringRes @Override public int getOutgoingCallTypeText() { diff --git a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java index 393b07a6b..6115c2f1c 100644 --- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java +++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java @@ -345,7 +345,7 @@ public final class NewSearchFragment extends Fragment if (!PermissionsUtil.hasLocationPermissions(getContext()) && !DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(getContext()) .getBoolean(KEY_LOCATION_PROMPT_DISMISSED, false)) { - if (adapter != null && isRegularSearch()) { + if (adapter != null && isRegularSearch() && !hasBeenDismissed()) { adapter.showLocationPermissionRequest( v -> requestLocationPermission(), v -> dismissLocationPermission()); } @@ -372,7 +372,8 @@ public final class NewSearchFragment extends Fragment requestPermissions(deniedPermissions, LOCATION_PERMISSION_REQUEST_CODE); } - private void dismissLocationPermission() { + @VisibleForTesting + public void dismissLocationPermission() { PreferenceManager.getDefaultSharedPreferences(getContext()) .edit() .putBoolean(KEY_LOCATION_PROMPT_DISMISSED, true) @@ -380,6 +381,11 @@ public final class NewSearchFragment extends Fragment adapter.hideLocationPermissionRequest(); } + private boolean hasBeenDismissed() { + return PreferenceManager.getDefaultSharedPreferences(getContext()) + .getBoolean(KEY_LOCATION_PROMPT_DISMISSED, false); + } + @Override public void onResume() { super.onResume(); diff --git a/java/com/android/dialer/searchfragment/list/SearchAdapter.java b/java/com/android/dialer/searchfragment/list/SearchAdapter.java index 29e4e2320..949c2a2c5 100644 --- a/java/com/android/dialer/searchfragment/list/SearchAdapter.java +++ b/java/com/android/dialer/searchfragment/list/SearchAdapter.java @@ -187,7 +187,7 @@ public final class SearchAdapter extends RecyclerView.Adapter<ViewHolder> this.allowClickListener = Assert.isNotNull(allowClickListener); this.dismissClickListener = Assert.isNotNull(dismissClickListener); if (searchCursorManager.showLocationPermissionRequest(true)) { - notifyItemRemoved(0); + notifyItemInserted(0); } } diff --git a/java/com/android/dialer/simulator/Simulator.java b/java/com/android/dialer/simulator/Simulator.java index f753e5f6b..4812fa5d6 100644 --- a/java/com/android/dialer/simulator/Simulator.java +++ b/java/com/android/dialer/simulator/Simulator.java @@ -30,6 +30,16 @@ public interface Simulator { ActionProvider getActionProvider(Context context); + /** The type of conference to emulate. */ + // TODO(b/67785540): add VoLTE and CDMA conference call + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + CONFERENCE_TYPE_GSM, + }) + @interface ConferenceType {} + + static final int CONFERENCE_TYPE_GSM = 1; + /** Information about a connection event. */ public static class Event { /** The type of connection event. */ @@ -44,6 +54,11 @@ public interface Simulator { STATE_CHANGE, DTMF, SESSION_MODIFY_REQUEST, + CALL_AUDIO_STATE_CHANGED, + CONNECTION_ADDED, + MERGE, + SEPARATE, + SWAP, }) public @interface Type {} @@ -56,6 +71,11 @@ public interface Simulator { public static final int STATE_CHANGE = 6; public static final int DTMF = 7; public static final int SESSION_MODIFY_REQUEST = 8; + public static final int CALL_AUDIO_STATE_CHANGED = 9; + public static final int CONNECTION_ADDED = 10; + public static final int MERGE = 11; + public static final int SEPARATE = 12; + public static final int SWAP = 13; @Type public final int type; /** Holds event specific information. For example, for DTMF this could be the keycode. */ diff --git a/java/com/android/dialer/simulator/impl/SimulatorConference.java b/java/com/android/dialer/simulator/impl/SimulatorConference.java new file mode 100644 index 000000000..7468b56b5 --- /dev/null +++ b/java/com/android/dialer/simulator/impl/SimulatorConference.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2017 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.dialer.simulator.impl; + +import android.support.annotation.NonNull; +import android.telecom.CallAudioState; +import android.telecom.Conference; +import android.telecom.Connection; +import android.telecom.PhoneAccountHandle; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.simulator.Simulator; +import com.android.dialer.simulator.Simulator.Event; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a conference call. When a user merges two phone calls we create an instance of this + * conference object and add it to the connection service. All operations such as hold and DTMF are + * then performed on this object. + */ +public final class SimulatorConference extends Conference implements SimulatorConnection.Listener { + static final int PROPERTY_GENERIC_CONFERENCE = 1 << 1; + + private final List<Listener> listeners = new ArrayList<>(); + private final List<Event> events = new ArrayList<>(); + private final int conferenceType; + + private SimulatorConference( + PhoneAccountHandle handle, @Simulator.ConferenceType int conferenceType) { + super(handle); + this.conferenceType = conferenceType; + setActive(); + } + + static SimulatorConference newGsmConference(PhoneAccountHandle handle) { + SimulatorConference simulatorConference = + new SimulatorConference(handle, Simulator.CONFERENCE_TYPE_GSM); + simulatorConference.setConnectionCapabilities( + Connection.CAPABILITY_MUTE + | Connection.CAPABILITY_SUPPORT_HOLD + | Connection.CAPABILITY_HOLD + | Connection.CAPABILITY_MANAGE_CONFERENCE); + return simulatorConference; + } + + public void addListener(@NonNull Listener listener) { + listeners.add(Assert.isNotNull(listener)); + } + + public void removeListener(@NonNull Listener listener) { + listeners.remove(Assert.isNotNull(listener)); + } + + @NonNull + public List<Event> getEvents() { + return events; + } + + @Override + public void onCallAudioStateChanged(CallAudioState state) { + LogUtil.enterBlock("SimulatorConference.onCallAudioStateChanged"); + onEvent(new Event(Event.CALL_AUDIO_STATE_CHANGED)); + } + + @Override + public void onConnectionAdded(Connection connection) { + LogUtil.enterBlock("SimulatorConference.onConnectionAdded"); + onEvent( + new Event( + Event.CONNECTION_ADDED, SimulatorSimCallManager.getConnectionTag(connection), null)); + ((SimulatorConnection) connection).addListener(this); + } + + @Override + public void onDisconnect() { + LogUtil.enterBlock("SimulatorConference.onDisconnect"); + onEvent(new Event(Event.DISCONNECT)); + } + + @Override + public void onHold() { + LogUtil.enterBlock("SimulatorConference.onHold"); + onEvent(new Event(Event.HOLD)); + } + + @Override + public void onMerge(Connection connection) { + LogUtil.i("SimulatorConference.onMerge", "connection: " + connection); + onEvent(new Event(Event.MERGE, SimulatorSimCallManager.getConnectionTag(connection), null)); + } + + @Override + public void onMerge() { + LogUtil.enterBlock("SimulatorConference.onMerge"); + onEvent(new Event(Event.MERGE)); + } + + @Override + public void onPlayDtmfTone(char c) { + LogUtil.enterBlock("SimulatorConference.onPlayDtmfTone"); + onEvent(new Event(Event.DTMF, Character.toString(c), null)); + } + + @Override + public void onSeparate(Connection connection) { + LogUtil.i("SimulatorConference.onSeparate", "connection: " + connection); + onEvent(new Event(Event.SEPARATE, SimulatorSimCallManager.getConnectionTag(connection), null)); + } + + @Override + public void onSwap() { + LogUtil.enterBlock("SimulatorConference.onSwap"); + onEvent(new Event(Event.SWAP)); + } + + @Override + public void onUnhold() { + LogUtil.enterBlock("SimulatorConference.onUnhold"); + onEvent(new Event(Event.UNHOLD)); + } + + @Override + public void onEvent(@NonNull SimulatorConnection connection, @NonNull Event event) { + if (conferenceType == Simulator.CONFERENCE_TYPE_GSM) { + onGsmEvent(connection, event); + } + } + + private void onGsmEvent(@NonNull SimulatorConnection connection, @NonNull Event event) { + if (event.type == Event.STATE_CHANGE + && Connection.stateToString(Connection.STATE_DISCONNECTED).equals(event.data2)) { + removeConnection(connection); + connection.removeListener(this); + if (getConnections().size() <= 1) { + // When only one connection exists, it's not conference call anymore + setDisconnected(connection.getDisconnectCause()); + destroy(); + } + } + } + + void onEvent(@NonNull Event event) { + events.add(Assert.isNotNull(event)); + for (Listener listener : new ArrayList<>(listeners)) { + listener.onEvent(this, event); + } + } + + /** Callback for when a new event arrives. */ + public interface Listener { + void onEvent(@NonNull SimulatorConference conference, @NonNull Event event); + } +} diff --git a/java/com/android/dialer/simulator/impl/SimulatorConferenceCreator.java b/java/com/android/dialer/simulator/impl/SimulatorConferenceCreator.java new file mode 100644 index 000000000..838b58dc2 --- /dev/null +++ b/java/com/android/dialer/simulator/impl/SimulatorConferenceCreator.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2017 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.dialer.simulator.impl; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.telecom.Conferenceable; +import android.telecom.Connection; +import android.telecom.DisconnectCause; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.ThreadUtil; +import com.android.dialer.simulator.Simulator; +import com.android.dialer.simulator.Simulator.Event; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** Creates a conference with a given number of participants. */ +final class SimulatorConferenceCreator + implements SimulatorConnectionService.Listener, + SimulatorConnection.Listener, + SimulatorConference.Listener { + private static final String EXTRA_CALL_COUNT = "call_count"; + + @NonNull private final Context context; + @NonNull private final List<String> connectionTags = new ArrayList<>(); + @Simulator.ConferenceType private final int conferenceType; + + public SimulatorConferenceCreator( + @NonNull Context context, @Simulator.ConferenceType int conferenceType) { + this.context = Assert.isNotNull(context); + this.conferenceType = conferenceType; + } + + void start(int callCount) { + SimulatorConnectionService.addListener(this); + addNextCall(callCount); + } + + private void addNextCall(int callCount) { + LogUtil.i("SimulatorConferenceCreator.addNextIncomingCall", "callCount: " + callCount); + if (callCount <= 0) { + LogUtil.i("SimulatorConferenceCreator.addNextCall", "done adding calls"); + return; + } + + String callerId = String.format(Locale.US, "+1-650-234%04d", callCount); + Bundle extras = new Bundle(); + extras.putInt(EXTRA_CALL_COUNT, callCount - 1); + connectionTags.add( + SimulatorSimCallManager.addNewIncomingCall(context, callerId, false /* isVideo */, extras)); + } + + @Override + public void onNewIncomingConnection(@NonNull SimulatorConnection connection) { + if (!isMyConnection(connection)) { + LogUtil.i("SimulatorConferenceCreator.onNewOutgoingConnection", "unknown connection"); + return; + } + + LogUtil.i("SimulatorConferenceCreator.onNewOutgoingConnection", "connection created"); + connection.addListener(this); + + // Telecom will force the connection to switch to DIALING when we return it. Wait until after + // we're returned it before changing call state. + ThreadUtil.postOnUiThread(() -> connection.setActive()); + + // Once the connection is active, go ahead and conference it and add the next call. + ThreadUtil.postDelayedOnUiThread( + () -> { + SimulatorConference conference = findCurrentConference(); + if (conference == null) { + Assert.checkArgument(conferenceType == Simulator.CONFERENCE_TYPE_GSM); + conference = + SimulatorConference.newGsmConference( + SimulatorSimCallManager.getSystemPhoneAccountHandle(context)); + conference.addListener(this); + SimulatorConnectionService.getInstance().addConference(conference); + } + updateConferenceableConnections(); + conference.addConnection(connection); + addNextCall(getCallCount(connection)); + }, + 1000); + } + + @Override + public void onNewOutgoingConnection(@NonNull SimulatorConnection connection) {} + + /** + * This is called when the user clicks the merge button. We create the initial conference + * automatically but with this method we can let the user split and merge calls as desired. + */ + @Override + public void onConference( + @NonNull SimulatorConnection connection1, @NonNull SimulatorConnection connection2) { + LogUtil.enterBlock("SimulatorConferenceCreator.onConference"); + if (!isMyConnection(connection1) || !isMyConnection(connection2)) { + LogUtil.i("SimulatorConferenceCreator.onConference", "unknown connections, ignoring"); + return; + } + + if (connection1.getConference() != null) { + connection1.getConference().addConnection(connection2); + } else if (connection2.getConference() != null) { + connection2.getConference().addConnection(connection1); + } else { + Assert.checkArgument(conferenceType == Simulator.CONFERENCE_TYPE_GSM); + SimulatorConference conference = + SimulatorConference.newGsmConference( + SimulatorSimCallManager.getSystemPhoneAccountHandle(context)); + conference.addConnection(connection1); + conference.addConnection(connection2); + conference.addListener(this); + SimulatorConnectionService.getInstance().addConference(conference); + } + } + + private boolean isMyConnection(@NonNull Connection connection) { + for (String connectionTag : connectionTags) { + if (connection.getExtras().getBoolean(connectionTag)) { + return true; + } + } + return false; + } + + private void updateConferenceableConnections() { + LogUtil.enterBlock("SimulatorConferenceCreator.updateConferenceableConnections"); + for (String connectionTag : connectionTags) { + SimulatorConnection connection = SimulatorSimCallManager.findConnectionByTag(connectionTag); + List<Conferenceable> conferenceables = getMyConferenceables(); + conferenceables.remove(connection); + conferenceables.remove(connection.getConference()); + connection.setConferenceables(conferenceables); + } + } + + private List<Conferenceable> getMyConferenceables() { + List<Conferenceable> conferenceables = new ArrayList<>(); + for (String connectionTag : connectionTags) { + SimulatorConnection connection = SimulatorSimCallManager.findConnectionByTag(connectionTag); + conferenceables.add(connection); + if (connection.getConference() != null + && !conferenceables.contains(connection.getConference())) { + conferenceables.add(connection.getConference()); + } + } + return conferenceables; + } + + @Nullable + private SimulatorConference findCurrentConference() { + for (String connectionTag : connectionTags) { + SimulatorConnection connection = SimulatorSimCallManager.findConnectionByTag(connectionTag); + if (connection.getConference() != null) { + return (SimulatorConference) connection.getConference(); + } + } + return null; + } + + private static int getCallCount(@NonNull Connection connection) { + return connection.getExtras().getInt(EXTRA_CALL_COUNT); + } + + @Override + public void onEvent(@NonNull SimulatorConnection connection, @NonNull Event event) { + switch (event.type) { + case Event.NONE: + throw Assert.createIllegalStateFailException(); + case Event.HOLD: + connection.setOnHold(); + break; + case Event.UNHOLD: + connection.setActive(); + break; + case Event.DISCONNECT: + connection.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); + break; + default: + LogUtil.i( + "SimulatorConferenceCreator.onEvent", "unexpected conference event: " + event.type); + break; + } + } + + @Override + public void onEvent(@NonNull SimulatorConference conference, @NonNull Event event) { + switch (event.type) { + case Event.MERGE: + int capabilities = conference.getConnectionCapabilities(); + capabilities |= Connection.CAPABILITY_SWAP_CONFERENCE; + conference.setConnectionCapabilities(capabilities); + break; + case Event.SEPARATE: + SimulatorConnection connectionToRemove = + SimulatorSimCallManager.findConnectionByTag(event.data1); + conference.removeConnection(connectionToRemove); + break; + case Event.DISCONNECT: + for (Connection connection : new ArrayList<>(conference.getConnections())) { + connection.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); + } + break; + default: + LogUtil.i( + "SimulatorConferenceCreator.onEvent", "unexpected conference event: " + event.type); + break; + } + } +} diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnection.java b/java/com/android/dialer/simulator/impl/SimulatorConnection.java index 70c1095dc..e4a34b51b 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorConnection.java +++ b/java/com/android/dialer/simulator/impl/SimulatorConnection.java @@ -41,7 +41,9 @@ public final class SimulatorConnection extends Connection { CAPABILITY_MUTE | CAPABILITY_SUPPORT_HOLD | CAPABILITY_HOLD - | CAPABILITY_CAN_UPGRADE_TO_VIDEO); + | CAPABILITY_CAN_UPGRADE_TO_VIDEO + | CAPABILITY_DISCONNECT_FROM_CONFERENCE + | CAPABILITY_SEPARATE_FROM_CONFERENCE); setVideoProvider(new SimulatorVideoProvider(context, this)); } @@ -108,7 +110,7 @@ public final class SimulatorConnection extends Connection { void onEvent(@NonNull Event event) { events.add(Assert.isNotNull(event)); - for (Listener listener : listeners) { + for (Listener listener : new ArrayList<>(listeners)) { listener.onEvent(this, event); } } diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java b/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java index 25d4a7240..465890cf0 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java +++ b/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java @@ -109,6 +109,19 @@ public class SimulatorConnectionService extends ConnectionService { return connection; } + @Override + public void onConference(Connection connection1, Connection connection2) { + LogUtil.i( + "SimulatorConnectionService.onConference", + "connection1: " + + SimulatorSimCallManager.getConnectionTag(connection1) + + ", connection2: " + + SimulatorSimCallManager.getConnectionTag(connection2)); + for (Listener listener : listeners) { + listener.onConference((SimulatorConnection) connection1, (SimulatorConnection) connection2); + } + } + private static Uri getPhoneNumber(ConnectionRequest request) { String phoneNumber = request.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER); return Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null); @@ -116,8 +129,11 @@ public class SimulatorConnectionService extends ConnectionService { /** Callback used to notify listeners when a new connection has been added. */ public interface Listener { - void onNewOutgoingConnection(SimulatorConnection connection); + void onNewOutgoingConnection(@NonNull SimulatorConnection connection); + + void onNewIncomingConnection(@NonNull SimulatorConnection connection); - void onNewIncomingConnection(SimulatorConnection connection); + void onConference( + @NonNull SimulatorConnection connection1, @NonNull SimulatorConnection connection2); } } diff --git a/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java b/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java index f85f46602..6d4a26278 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java +++ b/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java @@ -62,6 +62,10 @@ final class SimulatorMissedCallCreator implements SimulatorConnectionService.Lis DISCONNECT_DELAY_MILLIS); } + @Override + public void onConference( + @NonNull SimulatorConnection connection1, @NonNull SimulatorConnection connection2) {} + private void addNextIncomingCall(int callCount) { if (callCount <= 0) { LogUtil.i("SimulatorMissedCallCreator.addNextIncomingCall", "done adding calls"); diff --git a/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java b/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java index 33eac51d1..00899fd69 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java +++ b/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java @@ -21,6 +21,7 @@ import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; +import android.telecom.Connection; import android.telecom.ConnectionRequest; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; @@ -47,6 +48,7 @@ public class SimulatorSimCallManager { private static final String SIM_CALL_MANAGER_ACCOUNT_ID = "SIMULATOR_ACCOUNT_ID"; private static final String VIDEO_PROVIDER_ACCOUNT_ID = "SIMULATOR_VIDEO_ACCOUNT_ID"; private static final String EXTRA_IS_SIMULATOR_CONNECTION = "is_simulator_connection"; + private static final String EXTRA_CONNECTION_TAG = "connection_tag"; static void register(@NonNull Context context) { LogUtil.enterBlock("SimulatorSimCallManager.register"); @@ -85,9 +87,7 @@ public class SimulatorSimCallManager { register(context); extras = new Bundle(extras); - extras.putBoolean(EXTRA_IS_SIMULATOR_CONNECTION, true); - String connectionTag = createUniqueConnectionTag(); - extras.putBoolean(connectionTag, true); + extras.putAll(createSimulatorConnectionExtras()); Bundle outgoingCallExtras = new Bundle(); outgoingCallExtras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras); @@ -102,7 +102,7 @@ public class SimulatorSimCallManager { } catch (SecurityException e) { throw Assert.createIllegalStateFailException("Unable to place call: " + e); } - return connectionTag; + return extras.getString(EXTRA_CONNECTION_TAG); } @NonNull @@ -123,14 +123,12 @@ public class SimulatorSimCallManager { extras = new Bundle(extras); extras.putString(TelephonyManager.EXTRA_INCOMING_NUMBER, callerId); - extras.putBoolean(EXTRA_IS_SIMULATOR_CONNECTION, true); - String connectionTag = createUniqueConnectionTag(); - extras.putBoolean(connectionTag, true); + extras.putAll(createSimulatorConnectionExtras()); TelecomManager telecomManager = context.getSystemService(TelecomManager.class); telecomManager.addNewIncomingCall( isVideo ? getVideoProviderHandle(context) : getSystemPhoneAccountHandle(context), extras); - return connectionTag; + return extras.getString(EXTRA_CONNECTION_TAG); } @NonNull @@ -167,7 +165,7 @@ public class SimulatorSimCallManager { } @NonNull - private static PhoneAccountHandle getSystemPhoneAccountHandle(@NonNull Context context) { + public static PhoneAccountHandle getSystemPhoneAccountHandle(@NonNull Context context) { TelecomManager telecomManager = context.getSystemService(TelecomManager.class); List<PhoneAccountHandle> handles; try { @@ -190,10 +188,37 @@ public class SimulatorSimCallManager { } @NonNull + public static String getConnectionTag(@NonNull Connection connection) { + String connectionTag = connection.getExtras().getString(EXTRA_CONNECTION_TAG); + return Assert.isNotNull(connectionTag); + } + + @NonNull + public static SimulatorConnection findConnectionByTag(@NonNull String connectionTag) { + Assert.isNotNull(connectionTag); + for (Connection connection : SimulatorConnectionService.getInstance().getAllConnections()) { + if (connection.getExtras().getBoolean(connectionTag)) { + return (SimulatorConnection) connection; + } + } + throw Assert.createIllegalStateFailException(); + } + + @NonNull private static String createUniqueConnectionTag() { int callId = new Random().nextInt(); return String.format("simulator_phone_call_%x", Math.abs(callId)); } + @NonNull + static Bundle createSimulatorConnectionExtras() { + Bundle extras = new Bundle(); + extras.putBoolean(EXTRA_IS_SIMULATOR_CONNECTION, true); + String connectionTag = createUniqueConnectionTag(); + extras.putString(EXTRA_CONNECTION_TAG, connectionTag); + extras.putBoolean(connectionTag, true); + return extras; + } + private SimulatorSimCallManager() {} } diff --git a/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java b/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java index 757658ddf..a843ec03f 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java +++ b/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java @@ -74,6 +74,10 @@ final class SimulatorSpamCallCreator implements SimulatorConnectionService.Liste DISCONNECT_DELAY_MILLIS); } + @Override + public void onConference( + @NonNull SimulatorConnection connection1, @NonNull SimulatorConnection connection2) {} + private void addNextIncomingCall(int callCount) { if (callCount <= 0) { LogUtil.i("SimulatorSpamCallCreator.addNextIncomingCall", "done adding calls"); diff --git a/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java b/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java index 3f00ab183..f7256a11c 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java +++ b/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java @@ -109,6 +109,10 @@ final class SimulatorVideoCall } } + @Override + public void onConference( + @NonNull SimulatorConnection connection1, @NonNull SimulatorConnection connection2) {} + private boolean isVideoAccountEnabled() { SimulatorSimCallManager.register(context); return context @@ -150,15 +154,12 @@ final class SimulatorVideoCall case Event.DISCONNECT: connection.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); break; - case Event.STATE_CHANGE: - break; - case Event.DTMF: - break; case Event.SESSION_MODIFY_REQUEST: ThreadUtil.postDelayedOnUiThread(() -> connection.handleSessionModifyRequest(event), 2000); break; default: - throw Assert.createIllegalStateFailException(); + LogUtil.i("SimulatorVideoCall.onEvent", "unexpected event: " + event.type); + break; } } } diff --git a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java index 8eefb48d9..f478d55d0 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java +++ b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java @@ -25,6 +25,7 @@ import android.view.ActionProvider; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.ThreadUtil; +import com.android.dialer.simulator.Simulator; import com.android.dialer.simulator.Simulator.Event; /** Entry point in the simulator to create voice calls. */ @@ -37,7 +38,10 @@ final class SimulatorVoiceCall return new SimulatorSubMenu(context) .addItem("Incoming call", () -> new SimulatorVoiceCall(context).addNewIncomingCall(false)) .addItem("Outgoing call", () -> new SimulatorVoiceCall(context).addNewOutgoingCall()) - .addItem("Spam call", () -> new SimulatorVoiceCall(context).addNewIncomingCall(true)); + .addItem("Spam call", () -> new SimulatorVoiceCall(context).addNewIncomingCall(true)) + .addItem( + "GSM conference", + () -> new SimulatorConferenceCreator(context, Simulator.CONFERENCE_TYPE_GSM).start(5)); } private SimulatorVoiceCall(@NonNull Context context) { @@ -62,21 +66,28 @@ final class SimulatorVoiceCall @Override public void onNewOutgoingConnection(@NonNull SimulatorConnection connection) { - if (connection.getExtras().getBoolean(connectionTag)) { + if (isMyConnection(connection)) { LogUtil.i("SimulatorVoiceCall.onNewOutgoingConnection", "connection created"); handleNewConnection(connection); - connection.setActive(); + + // Telecom will force the connection to switch to Dialing when we return it. Wait until after + // we're returned it before changing call state. + ThreadUtil.postOnUiThread(connection::setActive); } } @Override public void onNewIncomingConnection(@NonNull SimulatorConnection connection) { - if (connection.getExtras().getBoolean(connectionTag)) { + if (isMyConnection(connection)) { LogUtil.i("SimulatorVoiceCall.onNewIncomingConnection", "connection created"); handleNewConnection(connection); } } + @Override + public void onConference( + @NonNull SimulatorConnection connection1, @NonNull SimulatorConnection connection2) {} + private void handleNewConnection(@NonNull SimulatorConnection connection) { connection.addListener(this); connection.setConnectionCapabilities( @@ -85,6 +96,10 @@ final class SimulatorVoiceCall | Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL); } + private boolean isMyConnection(@NonNull Connection connection) { + return connection.getExtras().getBoolean(connectionTag); + } + @Override public void onEvent(@NonNull SimulatorConnection connection, @NonNull Event event) { switch (event.type) { @@ -105,15 +120,12 @@ final class SimulatorVoiceCall case Event.DISCONNECT: connection.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); break; - case Event.STATE_CHANGE: - break; - case Event.DTMF: - break; case Event.SESSION_MODIFY_REQUEST: ThreadUtil.postDelayedOnUiThread(() -> connection.handleSessionModifyRequest(event), 2000); break; default: - throw Assert.createIllegalStateFailException(); + LogUtil.i("SimulatorVoiceCall.onEvent", "unexpected event: " + event.type); + break; } } } |