diff --git a/res/layout/frag_call_list.xml b/res/layout/frag_call_list.xml index e5de2c2b3906c6db96654dfb644875fcdfb68477..dec110d6d6ddd970a42fcf3bdef32c190f780f6e 100644 --- a/res/layout/frag_call_list.xml +++ b/res/layout/frag_call_list.xml @@ -34,52 +34,10 @@ as that of the covered work. android:layout_width="match_parent" android:layout_height="match_parent" > - <RelativeLayout - android:id="@+id/calls_layouts" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_alignParentTop="true" - android:layout_margin="10dp" - android:background="@drawable/item_generic_selector" > - - <LinearLayout - android:id="@+id/linear1" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:layout_alignParentTop="true" - android:weightSum="4" > - - <TextView - android:id="@+id/calls_counter" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - android:gravity="center" - android:textSize="40sp" /> - - <TextView - android:id="@+id/textView2" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="3" - android:text="@string/home_calls_title" - android:textSize="30sp" /> - </LinearLayout> - - <ListView - android:id="@+id/calls_list" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@+id/linear1" > - </ListView> - </RelativeLayout> - <RelativeLayout android:id="@+id/confs_layouts" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@+id/calls_layouts" android:layout_margin="10dp" android:background="@drawable/item_generic_selector" > diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 0c4fb64fa1830df3f17cb67889910c70c6a54c6c..88fbe807463a8f61821ba20b4ea790eb5a5e865f 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -76,7 +76,7 @@ as that of the covered work. <string name="detail_hist_call_number">Appeler %1$s</string> <!-- Home Fragment --> - <string name="home_conferences_title">Conférences</string> + <string name="home_conferences_title">Conversations</string> <string name="home_calls_title">Appels</string> <string name="home_transfering">Transfert de %1$s à %1$s</string> <string name="home_transfer_complet">Transfert terminé</string> diff --git a/res/values/strings.xml b/res/values/strings.xml index 3edc96c3ec59435cfb7728d3a2365d40b959df1e..5c4052bceae9d1e6f81699b1cae933a3f93b83eb 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -76,7 +76,7 @@ as that of the covered work. <string name="detail_hist_call_number">Call %1$s</string> <!-- Home Fragment --> - <string name="home_conferences_title">Conferences</string> + <string name="home_conferences_title">Conversations</string> <string name="home_calls_title">Calls</string> <string name="home_transfering">Transferring %1$s to %1$s</string> <string name="home_transfer_complet">Transfer complete</string> diff --git a/src/org/sflphone/adapters/SectionsPagerAdapter.java b/src/org/sflphone/adapters/SectionsPagerAdapter.java index 485c005300ba8ea87310db639294ac695c14b4fb..a92ccad1cae4164b9bf9251b38890af11e1dab13 100644 --- a/src/org/sflphone/adapters/SectionsPagerAdapter.java +++ b/src/org/sflphone/adapters/SectionsPagerAdapter.java @@ -112,16 +112,6 @@ public class SectionsPagerAdapter extends android.support.v13.app.FragmentStateP return null; } - public void updateHome() { - try { - ((CallListFragment) fragments.get(1)).updateLists(); - } catch (RemoteException e) { - e.printStackTrace(); - } catch (Exception e1) { - e1.printStackTrace(); - } - } - @Override public int getPageIconResId(int position) { switch (position) { diff --git a/src/org/sflphone/client/HomeActivity.java b/src/org/sflphone/client/HomeActivity.java index b56dce6256366f5f2ece71ec633a4edd85d2e111..85819596629158611afd7f1dced9eed3a81e5f3c 100644 --- a/src/org/sflphone/client/HomeActivity.java +++ b/src/org/sflphone/client/HomeActivity.java @@ -97,7 +97,7 @@ public class HomeActivity extends FragmentActivity implements DialingFragment.Ca private ISipService service; public static final int REQUEST_CODE_PREFERENCES = 1; - private static final int REQUEST_CODE_CALL = 3; + public static final int REQUEST_CODE_CALL = 3; SlidingUpPanelLayout mContactDrawer; private DrawerLayout mNavigationDrawer; @@ -597,14 +597,6 @@ public class HomeActivity extends FragmentActivity implements DialingFragment.Ca mContactDrawer.expandPane(); } - @Override - public void selectedCall(Conference c) { - Intent intent = new Intent().setClass(this, CallActivity.class); - intent.putExtra("resuming", true); - intent.putExtra("conference", c); - startActivityForResult(intent, REQUEST_CODE_CALL); - } - @Override public void setDragView(RelativeLayout relativeLayout) { mContactDrawer.setDragView(relativeLayout); diff --git a/src/org/sflphone/fragments/CallListFragment.java b/src/org/sflphone/fragments/CallListFragment.java index a324b6a1ba04ba58fe55f7fa415b933974b2acda..ea63b7ea15333a3eb619b0a573e36710878b726b 100644 --- a/src/org/sflphone/fragments/CallListFragment.java +++ b/src/org/sflphone/fragments/CallListFragment.java @@ -30,32 +30,15 @@ */ package org.sflphone.fragments; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Observable; -import java.util.Observer; - -import android.content.IntentFilter; -import org.sflphone.R; -import org.sflphone.interfaces.CallInterface; -import org.sflphone.model.CallTimer; -import org.sflphone.model.Conference; -import org.sflphone.receivers.CallReceiver; -import org.sflphone.service.CallManagerCallBack; -import org.sflphone.service.ISipService; - import android.app.Activity; +import android.app.Fragment; import android.content.ClipData; import android.content.ClipData.Item; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.graphics.Color; -import android.os.Bundle; -import android.os.Handler; -import android.os.RemoteException; -import android.os.SystemClock; -import android.os.Vibrator; -import android.app.Fragment; +import android.os.*; import android.util.Log; import android.view.DragEvent; import android.view.LayoutInflater; @@ -63,21 +46,30 @@ import android.view.View; import android.view.View.DragShadowBuilder; import android.view.View.OnDragListener; import android.view.ViewGroup; -import android.widget.AdapterView; +import android.widget.*; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.BaseAdapter; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; +import org.sflphone.R; +import org.sflphone.client.CallActivity; +import org.sflphone.client.HomeActivity; +import org.sflphone.interfaces.CallInterface; +import org.sflphone.model.Conference; +import org.sflphone.receivers.CallReceiver; +import org.sflphone.service.CallManagerCallBack; +import org.sflphone.service.ISipService; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Observable; +import java.util.Observer; + +public class CallListFragment extends Fragment implements CallInterface { -public class CallListFragment extends Fragment implements CallInterface{ private static final String TAG = CallListFragment.class.getSimpleName(); private Callbacks mCallbacks = sDummyCallbacks; - private TextView nb_calls, nb_confs; - CallListAdapter confs_adapter, calls_adapter; - CallTimer timer; + private TextView nb_confs; + CallListAdapter confs_adapter; CallReceiver callReceiver; public static final int REQUEST_TRANSFER = 10; @@ -94,9 +86,6 @@ public class CallListFragment extends Fragment implements CallInterface{ return null; } - @Override - public void selectedCall(Conference c) { - } }; @Override @@ -105,11 +94,9 @@ public class CallListFragment extends Fragment implements CallInterface{ String cID = b.getString("CallID"); String state = b.getString("State"); Log.i(TAG, "callStateChanged" + cID + " " + state); - try { - updateLists(); - } catch (RemoteException e) { - e.printStackTrace(); - } + + updateLists(); + } @Override @@ -123,21 +110,20 @@ public class CallListFragment extends Fragment implements CallInterface{ @Override public void confCreated(Intent intent) { - try { - updateLists(); - } catch (RemoteException e) { - e.printStackTrace(); - } + Log.i(TAG, "confCreated"); + updateLists(); } @Override public void confRemoved(Intent intent) { - + Log.i(TAG, "confRemoved"); + updateLists(); } @Override public void confChanged(Intent intent) { - + Log.i(TAG, "confChanged"); + updateLists(); } @Override @@ -149,11 +135,7 @@ public class CallListFragment extends Fragment implements CallInterface{ * The Activity calling this fragment has to implement this interface */ public interface Callbacks { - public ISipService getService(); - - public void selectedCall(Conference c); - } @Override @@ -176,7 +158,6 @@ public class CallListFragment extends Fragment implements CallInterface{ int minutes = seconds / 60; seconds = seconds % 60; - calls_adapter.notifyDataSetChanged(); confs_adapter.notifyDataSetChanged(); mHandler.postAtTime(this, start + (((minutes * 60) + seconds + 1) * 1000)); } @@ -193,14 +174,10 @@ public class CallListFragment extends Fragment implements CallInterface{ intentFilter.addAction(CallManagerCallBack.CALL_STATE_CHANGED); getActivity().registerReceiver(callReceiver, intentFilter); if (mCallbacks.getService() != null) { - try { - updateLists(); - if (!calls_adapter.isEmpty() || !confs_adapter.isEmpty()) { - mHandler.postDelayed(mUpdateTimeTask, 0); - } - - } catch (RemoteException e) { - Log.e(TAG, e.toString()); + + updateLists(); + if (!confs_adapter.isEmpty()) { + mHandler.postDelayed(mUpdateTimeTask, 0); } } @@ -208,27 +185,16 @@ public class CallListFragment extends Fragment implements CallInterface{ @SuppressWarnings("unchecked") // No proper solution with HashMap runtime cast - public void updateLists() throws RemoteException { - HashMap<String, Conference> confs = (HashMap<String, Conference>) mCallbacks.getService().getConferenceList(); - Log.i(TAG, "There are " + confs.size()); - sortConferences(confs); - } - - private void sortConferences(HashMap<String, Conference> conferences) { - - ArrayList<Conference> multiConfs = new ArrayList<Conference>(); - ArrayList<Conference> oneToOneConfs = new ArrayList<Conference>(); - for (Conference conf : conferences.values()) { - if (conf.hasMultipleParticipants()) - multiConfs.add(conf); - else - oneToOneConfs.add(conf); + public void updateLists() { + HashMap<String, Conference> confs; + try { + confs = (HashMap<String, Conference>) mCallbacks.getService().getConferenceList(); + nb_confs.setText("" + confs.size()); + confs_adapter.updateDataset(new ArrayList<Conference>(confs.values())); + } catch (RemoteException e) { + e.printStackTrace(); } - nb_confs.setText("" + multiConfs.size()); - nb_calls.setText("" + oneToOneConfs.size()); - confs_adapter.updateDataset(multiConfs); - calls_adapter.updateDataset(oneToOneConfs); } @Override @@ -261,18 +227,11 @@ public class CallListFragment extends Fragment implements CallInterface{ Log.i(TAG, "onCreateView"); View inflatedView = inflater.inflate(R.layout.frag_call_list, container, false); - nb_calls = (TextView) inflatedView.findViewById(R.id.calls_counter); nb_confs = (TextView) inflatedView.findViewById(R.id.confs_counter); confs_adapter = new CallListAdapter(getActivity()); ((ListView) inflatedView.findViewById(R.id.confs_list)).setAdapter(confs_adapter); - - calls_adapter = new CallListAdapter(getActivity()); - ((ListView) inflatedView.findViewById(R.id.calls_list)).setAdapter(calls_adapter); - ((ListView) inflatedView.findViewById(R.id.calls_list)).setOnItemClickListener(callClickListener); ((ListView) inflatedView.findViewById(R.id.confs_list)).setOnItemClickListener(callClickListener); - - ((ListView) inflatedView.findViewById(R.id.calls_list)).setOnItemLongClickListener(mItemLongClickListener); ((ListView) inflatedView.findViewById(R.id.confs_list)).setOnItemLongClickListener(mItemLongClickListener); return inflatedView; @@ -282,7 +241,10 @@ public class CallListFragment extends Fragment implements CallInterface{ @Override public void onItemClick(AdapterView<?> arg0, View v, int arg2, long arg3) { - mCallbacks.selectedCall((Conference) v.getTag()); + Intent intent = new Intent().setClass(getActivity(), CallActivity.class); + intent.putExtra("resuming", true); + intent.putExtra("conference", (Conference) v.getTag()); + startActivityForResult(intent, HomeActivity.REQUEST_CODE_CALL); } }; @@ -442,7 +404,7 @@ public class CallListFragment extends Fragment implements CallInterface{ @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - Conference transfer = null; + Conference transfer; if (requestCode == REQUEST_TRANSFER) { switch (resultCode) { case 0: @@ -451,9 +413,9 @@ public class CallListFragment extends Fragment implements CallInterface{ try { mCallbacks.getService().attendedTransfer(transfer.getParticipants().get(0).getCallId(), c.getParticipants().get(0).getCallId()); - calls_adapter.remove(transfer); - calls_adapter.remove(c); - calls_adapter.notifyDataSetChanged(); + confs_adapter.remove(transfer); + confs_adapter.remove(c); + confs_adapter.notifyDataSetChanged(); } catch (RemoteException e) { // TODO Auto-generated catch block e.printStackTrace(); @@ -496,7 +458,7 @@ public class CallListFragment extends Fragment implements CallInterface{ private void bindCalls(Conference call_to_add, Conference call_target) { try { - Log.i(TAG, "joining calls:"+ call_to_add.getId() + " and " + call_target.getId()); + Log.i(TAG, "joining calls:" + call_to_add.getId() + " and " + call_target.getId()); if (call_target.hasMultipleParticipants() && !call_to_add.hasMultipleParticipants()) { diff --git a/src/org/sflphone/history/HistoryCall.java b/src/org/sflphone/history/HistoryCall.java index ac13f4888ab1d0a029fd373ef2d50ebe799082ac..972a1c44bfd752923dd172f0111fb67177304929 100644 --- a/src/org/sflphone/history/HistoryCall.java +++ b/src/org/sflphone/history/HistoryCall.java @@ -56,7 +56,7 @@ public class HistoryCall implements Parcelable { @DatabaseField boolean missed; @DatabaseField - String direction; + int direction; @DatabaseField String recordPath; @DatabaseField @@ -79,8 +79,8 @@ public class HistoryCall implements Parcelable { call_end = call.getTimestampEnd_(); accountID = call.getAccount().getAccountID(); number = call.getContact().getPhones().get(0).getNumber(); - missed = call.isRinging() || call.isIncoming(); - direction = call.getCallTypeString(); + missed = call.isRinging() && call.isIncoming(); + direction = call.getCallType(); recordPath = call.getRecordPath(); contactID = call.getContact().getId(); callID = call.getCallId(); @@ -91,7 +91,14 @@ public class HistoryCall implements Parcelable { } public String getDirection() { - return direction; + switch (direction) { + case SipCall.direction.CALL_TYPE_INCOMING: + return "CALL_TYPE_INCOMING"; + case SipCall.direction.CALL_TYPE_OUTGOING: + return "CALL_TYPE_OUTGOING"; + default: + return "CALL_TYPE_UNDETERMINED"; + } } public String getDate() { @@ -145,7 +152,7 @@ public class HistoryCall implements Parcelable { dest.writeString(accountID); dest.writeString(number); dest.writeByte((byte) (missed ? 1 : 0)); - dest.writeString(direction); + dest.writeInt(direction); dest.writeString(recordPath); dest.writeLong(contactID); dest.writeString(callID); @@ -167,7 +174,7 @@ public class HistoryCall implements Parcelable { accountID = in.readString(); number = in.readString(); missed = in.readByte() == 1 ? true : false; - direction = in.readString(); + direction = in.readInt(); recordPath = in.readString(); contactID = in.readLong(); callID = in.readString(); @@ -178,7 +185,7 @@ public class HistoryCall implements Parcelable { } public boolean isIncoming() { - return true; + return direction == SipCall.direction.CALL_TYPE_INCOMING; } public boolean isMissed() { diff --git a/src/org/sflphone/history/HistoryManager.java b/src/org/sflphone/history/HistoryManager.java index 70108e1f86685d6573299c3b28a28431cfe02371..bc90274f12633558bd68a9d0bad0f2275ec18760 100644 --- a/src/org/sflphone/history/HistoryManager.java +++ b/src/org/sflphone/history/HistoryManager.java @@ -65,6 +65,10 @@ public class HistoryManager { return true; } + /* + * Necessary when user hang up a call in a Conference + * The call creates an HistoryCall, but the conference still goes on + */ public boolean insertNewEntry(SipCall toInsert){ return true; } diff --git a/src/org/sflphone/model/SipCall.java b/src/org/sflphone/model/SipCall.java index a1f4a1e8ac4f17aaa08a04363b1df7012189af83..1b1e6cbe56c30e78eca2dd78d9f1545252438baf 100644 --- a/src/org/sflphone/model/SipCall.java +++ b/src/org/sflphone/model/SipCall.java @@ -72,7 +72,7 @@ public class SipCall implements Parcelable { timestampEnd_ = in.readLong(); } - public SipCall(String id, Account account, int call_type, int call_state, int media_state, CallContact c) { + private SipCall(String id, Account account, int call_type, int call_state, int media_state, CallContact c) { mCallID = id; mAccount = account; mCallType = call_type; @@ -89,6 +89,10 @@ public class SipCall implements Parcelable { return ""; } + public int getCallType() { + return mCallType; + } + public interface direction { public static final int CALL_TYPE_INCOMING = 1; public static final int CALL_TYPE_OUTGOING = 2; @@ -314,7 +318,6 @@ public class SipCall implements Parcelable { public boolean isOutGoing() { if (mCallType == direction.CALL_TYPE_OUTGOING) return true; - return false; } diff --git a/src/org/sflphone/service/CallManagerCallBack.java b/src/org/sflphone/service/CallManagerCallBack.java index de06377852385269814109cd102c7eadf566f052..40ae6ac307d3d7ecc81d0bb57758143ec772e6dc 100644 --- a/src/org/sflphone/service/CallManagerCallBack.java +++ b/src/org/sflphone/service/CallManagerCallBack.java @@ -243,43 +243,51 @@ public class CallManagerCallBack extends Callback { @Override public void on_conference_removed(String confID) { + Log.i(TAG, "on_conference_removed:"); Intent intent = new Intent(CONF_REMOVED); intent.putExtra("confID", confID); Conference toReInsert = mService.getConferences().get(confID); - /*for (int i = 0; i < toDestroy.getParticipants().size(); ++i) { - mService.getCurrentCalls().put(toDestroy.getParticipants().get(i).getCallId(), toDestroy.getParticipants().get(i)); - }*/ + for (SipCall call : toReInsert.getParticipants()) { + mService.getConferences().put(call.getCallId(), new Conference(call)); + } mService.getConferences().remove(confID); - mService.getConferences().put(toReInsert.getId(), toReInsert); mService.sendBroadcast(intent); } @Override public void on_conference_state_changed(String confID, String state) { - + Log.i(TAG, "on_conference_state_changed:"); Intent intent = new Intent(CONF_CHANGED); intent.putExtra("confID", confID); intent.putExtra("State", state); + mService.getConferences().get(confID).setCallState(confID, state); - StringVect all_participants = mService.getCallManagerJNI().getParticipantList(intent.getStringExtra("confID")); - for (int i = 0; i < all_participants.size(); ++i) { - if (mService.getConferences().get(confID).getParticipants().size() < all_participants.size() - && mService.getConferences().get(all_participants.get(i)) != null) { // We need to add the new participant to the conf - mService.getConferences().get(confID).addParticipant(mService.getConferences().get(all_participants.get(i)).getCallById(all_participants.get(i))); - mService.getConferences().remove(all_participants.get(i)); - mService.getConferences().get(confID).setCallState(confID, intent.getStringExtra("State")); - mService.sendBroadcast(intent); - return; + Log.i(TAG, "Received:" + intent.getAction()); + Log.i(TAG, "State:" + state); + + Conference toModify = mService.getConferences().get(confID); + + ArrayList<String> newParticipants = SwigNativeConverter.convertSwigToNative(mService.getCallManagerJNI().getParticipantList(intent.getStringExtra("confID"))); + + if (toModify.getParticipants().size() < newParticipants.size()) { + // We need to add the new participant to the conf + for (int i = 0; i < newParticipants.size(); ++i) { + if(toModify.getCallById(newParticipants.get(i))==null){ + mService.addCallToConference(toModify.getId(), newParticipants.get(i)); + } } - } + } else if (toModify.getParticipants().size() > newParticipants.size()) { - Log.i(TAG, "Received" + intent.getAction()); - if (mService.getConferences().get(confID) != null) { - mService.getConferences().get(confID).setCallState(confID, state); - mService.sendBroadcast(intent); + for (SipCall participant : toModify.getParticipants()) { + if (!newParticipants.contains(participant.getCallId())) { + mService.removeCallFromConference(toModify.getId(), participant.getCallId()); + } + } } + + mService.sendBroadcast(intent); } @Override diff --git a/src/org/sflphone/service/SipService.java b/src/org/sflphone/service/SipService.java index 78221ba144ee9dc0b5f95b5908cec1c39b729c0f..c5de94d739ac78c20c49c58929347dc64e040e29 100644 --- a/src/org/sflphone/service/SipService.java +++ b/src/org/sflphone/service/SipService.java @@ -83,6 +83,33 @@ public class SipService extends Service { return mConferences; } + public void addCallToConference(String confId, String callId) { + if(mConferences.get(callId) != null){ + // We add a simple call to a conference + mConferences.get(confId).addParticipant(mConferences.get(callId).getParticipants().get(0)); + mConferences.remove(callId); + } else { + Iterator<Map.Entry<String, Conference>> it = mConferences.entrySet().iterator(); + while (it.hasNext()) { + Conference tmp = it.next().getValue(); + for (SipCall c : tmp.getParticipants()) { + if (c.getCallId().contentEquals(callId)) { + mConferences.get(confId).addParticipant(c); + mConferences.get(tmp.getId()).removeParticipant(c.getCallId()); + } + } + } + } + + } + + public void removeCallFromConference(String confId, String callId) { + SipCall call = mConferences.get(confId).getCallById(callId); + Conference separate = new Conference(call); + mConferences.put(separate.getId(), separate); + } + + @Override public boolean onUnbind(Intent i) { super.onUnbind(i); @@ -125,7 +152,7 @@ public class SipService extends Service { Log.i(TAG, "onDestroy"); /* called once by stopService() */ mNotificationManager.onServiceDestroy(); - + mMediaManager.stopService(); getExecutor().execute(new FinalizeRunnable()); super.onDestroy(); @@ -358,7 +385,8 @@ public class SipService extends Service { protected void doRun() throws SameThreadException { Log.i(TAG, "SipService.hangUp() thread running..."); callManagerJNI.hangUp(callID); - mMediaManager.abandonAudioFocus(); + if(mConferences.size() == 0) + mMediaManager.abandonAudioFocus(); } }); } diff --git a/src/org/sflphone/utils/SwigNativeConverter.java b/src/org/sflphone/utils/SwigNativeConverter.java index aa6fce535e7d68916e329aac90f84e4c87919528..306d58047a7a165fbaa16c5aa9280d44bf2919dc 100644 --- a/src/org/sflphone/utils/SwigNativeConverter.java +++ b/src/org/sflphone/utils/SwigNativeConverter.java @@ -42,6 +42,7 @@ import org.sflphone.account.AccountDetailSrtp; import org.sflphone.account.AccountDetailTls; import org.sflphone.service.ServiceConstants; import org.sflphone.service.StringMap; +import org.sflphone.service.StringVect; import org.sflphone.service.VectMap; public class SwigNativeConverter { @@ -179,4 +180,11 @@ public class SwigNativeConverter { return toReturn; } + public static ArrayList<String> convertSwigToNative(StringVect vector) { + ArrayList<String> toReturn = new ArrayList<String>(); + for (int i = 0; i < vector.size(); ++i) { + toReturn.add(vector.get(i)); + } + return toReturn; + } }