From 450c96108c73569dbe13a9e91dc35b34e940aecc Mon Sep 17 00:00:00 2001 From: weijiaxu Date: Fri, 17 Nov 2017 10:33:02 -0800 Subject: Fix dialer simulator for conference calling funcitonality. Updated the following contents: 1.Fix the order of spawning connections for GSM conference. 2.Make VOLTE conference call more realistic. 3.Fix minor bugs about simulator. 4.Add SimulatorConnectionsBank class to store connection tags created by simulator. 5.Fix tests influenced by SimulatorConnectionsBank. WANT_LGTM=wangqi Bug: 67785540 Test: In dialer lab. PiperOrigin-RevId: 176127584 Change-Id: I846174b97ed9329df6347583c41095f45f43494b --- java/com/android/dialer/simulator/Simulator.java | 13 ++ .../dialer/simulator/SimulatorComponent.java | 2 + .../dialer/simulator/SimulatorConnectionsBank.java | 55 ++++++++ .../simulator/impl/SimulatorConferenceCreator.java | 119 +++++++---------- .../dialer/simulator/impl/SimulatorConnection.java | 6 + .../simulator/impl/SimulatorConnectionService.java | 20 ++- .../impl/SimulatorConnectionsBankImpl.java | 147 +++++++++++++++++++++ .../dialer/simulator/impl/SimulatorModule.java | 6 + .../dialer/simulator/impl/SimulatorVoiceCall.java | 2 + 9 files changed, 297 insertions(+), 73 deletions(-) create mode 100644 java/com/android/dialer/simulator/SimulatorConnectionsBank.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorConnectionsBankImpl.java (limited to 'java/com/android/dialer/simulator') diff --git a/java/com/android/dialer/simulator/Simulator.java b/java/com/android/dialer/simulator/Simulator.java index 2094b420e..d75d10e82 100644 --- a/java/com/android/dialer/simulator/Simulator.java +++ b/java/com/android/dialer/simulator/Simulator.java @@ -42,6 +42,19 @@ public interface Simulator { static final int CONFERENCE_TYPE_GSM = 1; static final int CONFERENCE_TYPE_VOLTE = 2; + /** The types of connection service listener events */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + ON_NEW_OUTGOING_CONNECTION, + ON_NEW_INCOMING_CONNECTION, + ON_CONFERENCE, + }) + @interface ConnectionServiceEventType {} + + static final int ON_NEW_OUTGOING_CONNECTION = 1; + static final int ON_NEW_INCOMING_CONNECTION = 2; + static final int ON_CONFERENCE = 3; + /** Information about a connection event. */ public static class Event { /** The type of connection event. */ diff --git a/java/com/android/dialer/simulator/SimulatorComponent.java b/java/com/android/dialer/simulator/SimulatorComponent.java index f14496b80..dee188281 100644 --- a/java/com/android/dialer/simulator/SimulatorComponent.java +++ b/java/com/android/dialer/simulator/SimulatorComponent.java @@ -26,6 +26,8 @@ public abstract class SimulatorComponent { public abstract Simulator getSimulator(); + public abstract SimulatorConnectionsBank getSimulatorConnectionsBank(); + public static SimulatorComponent get(Context context) { return ((HasComponent) ((HasRootComponent) context.getApplicationContext()).component()) .simulatorComponent(); diff --git a/java/com/android/dialer/simulator/SimulatorConnectionsBank.java b/java/com/android/dialer/simulator/SimulatorConnectionsBank.java new file mode 100644 index 000000000..23c00424f --- /dev/null +++ b/java/com/android/dialer/simulator/SimulatorConnectionsBank.java @@ -0,0 +1,55 @@ +/* + * 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; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.telecom.Connection; +import com.android.dialer.simulator.Simulator.ConferenceType; +import java.util.List; + +/** + * Used to create a shared connections bank which contains methods to manipulate connections. This + * is used mainly for conference calling. + */ +public interface SimulatorConnectionsBank { + + /** Add a connection into bank. */ + void add(Connection connection); + + /** Remove a connection from bank. */ + void remove(Connection connection); + + /** Merge all existing connections created by simulator into a conference. */ + void mergeAllConnections(@ConferenceType int conferenceType, Context context); + + /** Set all connections created by simulator to disconnected. */ + void disconnectAllConnections(); + + /** + * Update conferenceable connections for all connections in bank (usually after adding a new + * connection). Before calling this method, make sure all connections are returned by + * ConnectionService. + */ + void updateConferenceableConnections(); + + /** Determine whether a connection is created by simulator. */ + boolean isSimulatorConnection(@NonNull Connection connection); + + /** Get all connections tags from bank. */ + List getConnectionTags(); +} diff --git a/java/com/android/dialer/simulator/impl/SimulatorConferenceCreator.java b/java/com/android/dialer/simulator/impl/SimulatorConferenceCreator.java index d0249938a..36c19956a 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorConferenceCreator.java +++ b/java/com/android/dialer/simulator/impl/SimulatorConferenceCreator.java @@ -19,8 +19,6 @@ 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; @@ -28,8 +26,9 @@ 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 com.android.dialer.simulator.SimulatorComponent; +import com.android.dialer.simulator.SimulatorConnectionsBank; import java.util.ArrayList; -import java.util.List; import java.util.Locale; /** Creates a conference with a given number of participants. */ @@ -38,32 +37,59 @@ final class SimulatorConferenceCreator SimulatorConnection.Listener, SimulatorConference.Listener { private static final String EXTRA_CALL_COUNT = "call_count"; - + private static final String RECONNECT = "reconnect"; @NonNull private final Context context; - @NonNull private final List connectionTags = new ArrayList<>(); + + private final SimulatorConnectionsBank simulatorConnectionsBank; + + private boolean onNewIncomingConnectionEnabled = false; + @Simulator.ConferenceType private final int conferenceType; public SimulatorConferenceCreator( @NonNull Context context, @Simulator.ConferenceType int conferenceType) { this.context = Assert.isNotNull(context); this.conferenceType = conferenceType; + simulatorConnectionsBank = SimulatorComponent.get(context).getSimulatorConnectionsBank(); } void start(int callCount) { + onNewIncomingConnectionEnabled = true; SimulatorConnectionService.addListener(this); - addNextCall(callCount); + if (conferenceType == Simulator.CONFERENCE_TYPE_VOLTE) { + addNextCall(callCount, true); + } else if (conferenceType == Simulator.CONFERENCE_TYPE_GSM) { + addNextCall(callCount, false); + } } - - private void addNextCall(int callCount) { + /** + * Add a call in a process of making a conference. + * + * @param callCount the remaining number of calls to make + * @param reconnect whether all connections should reconnect once (connections are reconnected + * once in making VoLTE conference) + */ + private void addNextCall(int callCount, boolean reconnect) { LogUtil.i("SimulatorConferenceCreator.addNextIncomingCall", "callCount: " + callCount); if (callCount <= 0) { LogUtil.i("SimulatorConferenceCreator.addNextCall", "done adding calls"); + if (reconnect) { + simulatorConnectionsBank.disconnectAllConnections(); + addNextCall(simulatorConnectionsBank.getConnectionTags().size(), false); + } else { + simulatorConnectionsBank.mergeAllConnections(conferenceType, context); + SimulatorConnectionService.removeListener(this); + } return; } - String callerId = String.format(Locale.US, "+1-650-234%04d", callCount); Bundle extras = new Bundle(); extras.putInt(EXTRA_CALL_COUNT, callCount - 1); + extras.putBoolean(RECONNECT, reconnect); + addConferenceCall(callerId, extras); + } + + private void addConferenceCall(String number, Bundle extras) { switch (conferenceType) { case Simulator.CONFERENCE_TYPE_VOLTE: extras.putBoolean("ISVOLTE", true); @@ -71,35 +97,25 @@ final class SimulatorConferenceCreator default: break; } - connectionTags.add( - SimulatorSimCallManager.addNewIncomingCall(context, callerId, false /* isVideo */, extras)); + SimulatorSimCallManager.addNewIncomingCall(context, number, false /* isVideo */, extras); } @Override public void onNewIncomingConnection(@NonNull SimulatorConnection connection) { - if (!isMyConnection(connection)) { + if (!onNewIncomingConnectionEnabled) { + return; + } + if (!simulatorConnectionsBank.isSimulatorConnection(connection)) { LogUtil.i("SimulatorConferenceCreator.onNewOutgoingConnection", "unknown connection"); return; } - LogUtil.i("SimulatorConferenceCreator.onNewOutgoingConnection", "connection created"); connection.addListener(this); - // Once the connection is active, go ahead and conference it and add the next call. ThreadUtil.postDelayedOnUiThread( () -> { - SimulatorConference conference = findCurrentConference(); - if (conference == null) { - conference = - SimulatorConference.newGsmConference( - SimulatorSimCallManager.getSystemPhoneAccountHandle(context)); - conference.addListener(this); - SimulatorConnectionService.getInstance().addConference(conference); - } - updateConferenceableConnections(); connection.setActive(); - conference.addConnection(connection); - addNextCall(getCallCount(connection)); + addNextCall(getCallCount(connection), shouldReconnect(connection)); }, 1000); } @@ -115,7 +131,8 @@ final class SimulatorConferenceCreator public void onConference( @NonNull SimulatorConnection connection1, @NonNull SimulatorConnection connection2) { LogUtil.enterBlock("SimulatorConferenceCreator.onConference"); - if (!isMyConnection(connection1) || !isMyConnection(connection2)) { + if (!simulatorConnectionsBank.isSimulatorConnection(connection1) + || !simulatorConnectionsBank.isSimulatorConnection(connection2)) { LogUtil.i("SimulatorConferenceCreator.onConference", "unknown connections, ignoring"); return; } @@ -125,7 +142,6 @@ final class SimulatorConferenceCreator } else if (connection2.getConference() != null) { connection2.getConference().addConnection(connection1); } else { - Assert.checkArgument(conferenceType == Simulator.CONFERENCE_TYPE_GSM); SimulatorConference conference = SimulatorConference.newGsmConference( SimulatorSimCallManager.getSystemPhoneAccountHandle(context)); @@ -136,54 +152,14 @@ final class SimulatorConferenceCreator } } - 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 conferenceables = getMyConferenceables(); - conferenceables.remove(connection); - conferenceables.remove(connection.getConference()); - connection.setConferenceables(conferenceables); - } - } - - private List getMyConferenceables() { - List 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); } + private static boolean shouldReconnect(@NonNull Connection connection) { + return connection.getExtras().getBoolean(RECONNECT); + } + @Override public void onEvent(@NonNull SimulatorConnection connection, @NonNull Event event) { switch (event.type) { @@ -222,6 +198,7 @@ final class SimulatorConferenceCreator for (Connection connection : new ArrayList<>(conference.getConnections())) { connection.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); } + conference.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); break; default: LogUtil.i( diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnection.java b/java/com/android/dialer/simulator/impl/SimulatorConnection.java index 168f5db98..2a24d8f37 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorConnection.java +++ b/java/com/android/dialer/simulator/impl/SimulatorConnection.java @@ -24,6 +24,8 @@ import android.telecom.VideoProfile; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.simulator.Simulator.Event; +import com.android.dialer.simulator.SimulatorComponent; +import com.android.dialer.simulator.SimulatorConnectionsBank; import java.util.ArrayList; import java.util.List; @@ -31,6 +33,7 @@ import java.util.List; public final class SimulatorConnection extends Connection { private final List listeners = new ArrayList<>(); private final List events = new ArrayList<>(); + private final SimulatorConnectionsBank simulatorConnectionsBank; private int currentState = STATE_NEW; SimulatorConnection(@NonNull Context context, @NonNull ConnectionRequest request) { @@ -47,6 +50,7 @@ public final class SimulatorConnection extends Connection { setConnectionCapabilities(getConnectionCapabilities() | CAPABILITY_SEPARATE_FROM_CONFERENCE); } setVideoProvider(new SimulatorVideoProvider(context, this)); + simulatorConnectionsBank = SimulatorComponent.get(context).getSimulatorConnectionsBank(); } public void addListener(@NonNull Listener listener) { @@ -71,6 +75,7 @@ public final class SimulatorConnection extends Connection { @Override public void onReject() { LogUtil.enterBlock("SimulatorConnection.onReject"); + simulatorConnectionsBank.remove(this); onEvent(new Event(Event.REJECT)); } @@ -89,6 +94,7 @@ public final class SimulatorConnection extends Connection { @Override public void onDisconnect() { LogUtil.enterBlock("SimulatorConnection.onDisconnect"); + simulatorConnectionsBank.remove(this); onEvent(new Event(Event.DISCONNECT)); } diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java b/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java index 465890cf0..e6bf99f3a 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java +++ b/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java @@ -28,6 +28,9 @@ import android.telephony.TelephonyManager; import android.widget.Toast; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.ThreadUtil; +import com.android.dialer.simulator.SimulatorComponent; +import com.android.dialer.simulator.SimulatorConnectionsBank; import java.util.ArrayList; import java.util.List; @@ -35,6 +38,7 @@ import java.util.List; public class SimulatorConnectionService extends ConnectionService { private static final List listeners = new ArrayList<>(); private static SimulatorConnectionService instance; + private SimulatorConnectionsBank simulatorConnectionsBank; public static SimulatorConnectionService getInstance() { return instance; @@ -52,12 +56,14 @@ public class SimulatorConnectionService extends ConnectionService { public void onCreate() { super.onCreate(); instance = this; + simulatorConnectionsBank = SimulatorComponent.get(this).getSimulatorConnectionsBank(); } @Override public void onDestroy() { LogUtil.enterBlock("SimulatorConnectionService.onDestroy"); instance = null; + simulatorConnectionsBank = null; super.onDestroy(); } @@ -78,7 +84,12 @@ public class SimulatorConnectionService extends ConnectionService { SimulatorConnection connection = new SimulatorConnection(this, request); connection.setDialing(); connection.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED); - + simulatorConnectionsBank.add(connection); + ThreadUtil.postOnUiThread( + () -> + SimulatorComponent.get(instance) + .getSimulatorConnectionsBank() + .updateConferenceableConnections()); for (Listener listener : listeners) { listener.onNewOutgoingConnection(connection); } @@ -102,7 +113,12 @@ public class SimulatorConnectionService extends ConnectionService { SimulatorConnection connection = new SimulatorConnection(this, request); connection.setRinging(); connection.setAddress(getPhoneNumber(request), TelecomManager.PRESENTATION_ALLOWED); - + simulatorConnectionsBank.add(connection); + ThreadUtil.postOnUiThread( + () -> + SimulatorComponent.get(instance) + .getSimulatorConnectionsBank() + .updateConferenceableConnections()); for (Listener listener : listeners) { listener.onNewIncomingConnection(connection); } diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnectionsBankImpl.java b/java/com/android/dialer/simulator/impl/SimulatorConnectionsBankImpl.java new file mode 100644 index 000000000..75f144fde --- /dev/null +++ b/java/com/android/dialer/simulator/impl/SimulatorConnectionsBankImpl.java @@ -0,0 +1,147 @@ +/* + * 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.support.annotation.NonNull; +import android.telecom.Conferenceable; +import android.telecom.Connection; +import android.telecom.DisconnectCause; +import com.android.dialer.common.LogUtil; +import com.android.dialer.simulator.Simulator; +import com.android.dialer.simulator.Simulator.ConferenceType; +import com.android.dialer.simulator.Simulator.Event; +import com.android.dialer.simulator.SimulatorConnectionsBank; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.inject.Inject; + +/** Wraps a list of connection tags and common methods around the connection tags list. */ +public class SimulatorConnectionsBankImpl + implements SimulatorConnectionsBank, SimulatorConference.Listener { + private final List connectionTags = new ArrayList<>(); + + @Inject + public SimulatorConnectionsBankImpl() {} + + @Override + public List getConnectionTags() { + return connectionTags; + } + + @Override + public void add(Connection connection) { + connectionTags.add(SimulatorSimCallManager.getConnectionTag(connection)); + } + + @Override + public void remove(Connection connection) { + connectionTags.remove(SimulatorSimCallManager.getConnectionTag(connection)); + } + + @Override + public void mergeAllConnections(@ConferenceType int conferenceType, Context context) { + SimulatorConference simulatorConference = null; + if (conferenceType == Simulator.CONFERENCE_TYPE_GSM) { + simulatorConference = + SimulatorConference.newGsmConference( + SimulatorSimCallManager.getSystemPhoneAccountHandle(context)); + } else if (conferenceType == Simulator.CONFERENCE_TYPE_VOLTE) { + simulatorConference = + SimulatorConference.newVoLteConference( + SimulatorSimCallManager.getSystemPhoneAccountHandle(context)); + } + Collection connections = + SimulatorConnectionService.getInstance().getAllConnections(); + for (Connection connection : connections) { + simulatorConference.addConnection(connection); + } + simulatorConference.addListener(this); + SimulatorConnectionService.getInstance().addConference(simulatorConference); + } + + @Override + public void disconnectAllConnections() { + Collection connections = + SimulatorConnectionService.getInstance().getAllConnections(); + for (Connection connection : connections) { + connection.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); + } + } + + @Override + public void updateConferenceableConnections() { + LogUtil.enterBlock("SimulatorConferenceCreator.updateConferenceableConnections"); + for (String connectionTag : connectionTags) { + SimulatorConnection connection = SimulatorSimCallManager.findConnectionByTag(connectionTag); + List conferenceables = getSimulatorConferenceables(); + conferenceables.remove(connection); + conferenceables.remove(connection.getConference()); + connection.setConferenceables(conferenceables); + } + } + + private List getSimulatorConferenceables() { + List 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; + } + + @Override + public boolean isSimulatorConnection(@NonNull Connection connection) { + for (String connectionTag : connectionTags) { + if (connection.getExtras().getBoolean(connectionTag)) { + return true; + } + } + return false; + } + + @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)); + } + conference.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/SimulatorModule.java b/java/com/android/dialer/simulator/impl/SimulatorModule.java index c0cca271b..2bc72c956 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorModule.java +++ b/java/com/android/dialer/simulator/impl/SimulatorModule.java @@ -17,6 +17,7 @@ package com.android.dialer.simulator.impl; import com.android.dialer.simulator.Simulator; +import com.android.dialer.simulator.SimulatorConnectionsBank; import dagger.Binds; import dagger.Module; import javax.inject.Singleton; @@ -27,4 +28,9 @@ public abstract class SimulatorModule { @Binds @Singleton public abstract Simulator bindsSimulator(SimulatorImpl simulator); + + @Binds + @Singleton + public abstract SimulatorConnectionsBank bindsSimulatorConnectionsBank( + SimulatorConnectionsBankImpl simulatorConnectionsBank); } diff --git a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java index d2eba6b03..451896b73 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java +++ b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java @@ -52,6 +52,8 @@ final class SimulatorVoiceCall private SimulatorVoiceCall(@NonNull Context context) { this.context = Assert.isNotNull(context); SimulatorConnectionService.addListener(this); + SimulatorConnectionService.addListener( + new SimulatorConferenceCreator(context, Simulator.CONFERENCE_TYPE_GSM)); } private void addNewIncomingCall(boolean isSpam) { -- cgit v1.2.3