1package eu.siacs.conversations.entities;
2
3import android.content.ComponentName;
4import android.content.ContentValues;
5import android.content.Context;
6import android.content.pm.PackageManager;
7import android.database.Cursor;
8import android.graphics.drawable.Icon;
9import android.net.Uri;
10import android.os.Build;
11import android.os.Bundle;
12import android.telecom.PhoneAccount;
13import android.telecom.PhoneAccountHandle;
14import android.telecom.TelecomManager;
15import android.text.TextUtils;
16
17import androidx.annotation.NonNull;
18
19import com.google.common.base.Strings;
20
21import org.json.JSONArray;
22import org.json.JSONException;
23import org.json.JSONObject;
24
25import java.util.ArrayList;
26import java.util.Collection;
27import java.util.HashSet;
28import java.util.List;
29import java.util.Locale;
30import java.util.Objects;
31
32import eu.siacs.conversations.Config;
33import eu.siacs.conversations.R;
34import eu.siacs.conversations.android.AbstractPhoneContact;
35import eu.siacs.conversations.android.JabberIdContact;
36import eu.siacs.conversations.services.AvatarService;
37import eu.siacs.conversations.services.QuickConversationsService;
38import eu.siacs.conversations.services.XmppConnectionService;
39import eu.siacs.conversations.utils.JidHelper;
40import eu.siacs.conversations.utils.UIHelper;
41import eu.siacs.conversations.xml.Element;
42import eu.siacs.conversations.xmpp.Jid;
43import eu.siacs.conversations.xmpp.jingle.RtpCapability;
44import eu.siacs.conversations.xmpp.pep.Avatar;
45
46public class Contact implements ListItem, Blockable {
47 public static final String TABLENAME = "contacts";
48
49 public static final String SYSTEMNAME = "systemname";
50 public static final String SERVERNAME = "servername";
51 public static final String PRESENCE_NAME = "presence_name";
52 public static final String JID = "jid";
53 public static final String OPTIONS = "options";
54 public static final String SYSTEMACCOUNT = "systemaccount";
55 public static final String PHOTOURI = "photouri";
56 public static final String KEYS = "pgpkey";
57 public static final String ACCOUNT = "accountUuid";
58 public static final String AVATAR = "avatar";
59 public static final String LAST_PRESENCE = "last_presence";
60 public static final String LAST_TIME = "last_time";
61 public static final String GROUPS = "groups";
62 public static final String RTP_CAPABILITY = "rtpCapability";
63 private String accountUuid;
64 private String systemName;
65 private String serverName;
66 private String presenceName;
67 private String commonName;
68 protected Jid jid;
69 private int subscription = 0;
70 private Uri systemAccount;
71 private String photoUri;
72 private final JSONObject keys;
73 private JSONArray groups = new JSONArray();
74 private JSONArray systemTags = new JSONArray();
75 private final Presences presences = new Presences();
76 protected Account account;
77 protected Avatar avatar;
78
79 private boolean mActive = false;
80 private long mLastseen = 0;
81 private String mLastPresence = null;
82 private RtpCapability.Capability rtpCapability;
83
84 public Contact(final String account, final String systemName, final String serverName, final String presenceName,
85 final Jid jid, final int subscription, final String photoUri,
86 final Uri systemAccount, final String keys, final String avatar, final long lastseen,
87 final String presence, final String groups, final RtpCapability.Capability rtpCapability) {
88 this.accountUuid = account;
89 this.systemName = systemName;
90 this.serverName = serverName;
91 this.presenceName = presenceName;
92 this.jid = jid;
93 this.subscription = subscription;
94 this.photoUri = photoUri;
95 this.systemAccount = systemAccount;
96 JSONObject tmpJsonObject;
97 try {
98 tmpJsonObject = (keys == null ? new JSONObject("") : new JSONObject(keys));
99 } catch (JSONException e) {
100 tmpJsonObject = new JSONObject();
101 }
102 this.keys = tmpJsonObject;
103 if (avatar != null) {
104 this.avatar = new Avatar();
105 this.avatar.sha1sum = avatar;
106 this.avatar.origin = Avatar.Origin.VCARD; //always assume worst
107 }
108 try {
109 this.groups = (groups == null ? new JSONArray() : new JSONArray(groups));
110 } catch (JSONException e) {
111 this.groups = new JSONArray();
112 }
113 this.mLastseen = lastseen;
114 this.mLastPresence = presence;
115 this.rtpCapability = rtpCapability;
116 }
117
118 public Contact(final Jid jid) {
119 this.jid = jid;
120 this.keys = new JSONObject();
121 }
122
123 public static Contact fromCursor(final Cursor cursor) {
124 final Jid jid;
125 try {
126 jid = Jid.of(cursor.getString(cursor.getColumnIndex(JID)));
127 } catch (final IllegalArgumentException e) {
128 // TODO: Borked DB... handle this somehow?
129 return null;
130 }
131 Uri systemAccount;
132 try {
133 systemAccount = Uri.parse(cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)));
134 } catch (Exception e) {
135 systemAccount = null;
136 }
137 return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)),
138 cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
139 cursor.getString(cursor.getColumnIndex(SERVERNAME)),
140 cursor.getString(cursor.getColumnIndex(PRESENCE_NAME)),
141 jid,
142 cursor.getInt(cursor.getColumnIndex(OPTIONS)),
143 cursor.getString(cursor.getColumnIndex(PHOTOURI)),
144 systemAccount,
145 cursor.getString(cursor.getColumnIndex(KEYS)),
146 cursor.getString(cursor.getColumnIndex(AVATAR)),
147 cursor.getLong(cursor.getColumnIndex(LAST_TIME)),
148 cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
149 cursor.getString(cursor.getColumnIndex(GROUPS)),
150 RtpCapability.Capability.of(cursor.getString(cursor.getColumnIndex(RTP_CAPABILITY))));
151 }
152
153 public String getDisplayName() {
154 if (isSelf() && TextUtils.isEmpty(this.systemName)) {
155 final String displayName = account.getDisplayName();
156 if (!Strings.isNullOrEmpty(displayName)) {
157 return displayName;
158 }
159 }
160 if (Config.X509_VERIFICATION && !TextUtils.isEmpty(this.commonName)) {
161 return this.commonName;
162 } else if (!TextUtils.isEmpty(this.systemName)) {
163 return this.systemName;
164 } else if (!TextUtils.isEmpty(this.serverName)) {
165 return this.serverName;
166 } else if (!TextUtils.isEmpty(this.presenceName) && ((QuickConversationsService.isQuicksy() && JidHelper.isQuicksyDomain(jid.getDomain())) || mutualPresenceSubscription())) {
167 return this.presenceName;
168 } else if (jid.getLocal() != null) {
169 return JidHelper.localPartOrFallback(jid);
170 } else {
171 return jid.getDomain().toEscapedString();
172 }
173 }
174
175 public String getPublicDisplayName() {
176 if (!TextUtils.isEmpty(this.presenceName)) {
177 return this.presenceName;
178 } else if (jid.getLocal() != null) {
179 return JidHelper.localPartOrFallback(jid);
180 } else {
181 return jid.getDomain().toEscapedString();
182 }
183 }
184
185 public String getProfilePhoto() {
186 return this.photoUri;
187 }
188
189 public Jid getJid() {
190 return jid;
191 }
192
193 public List<Tag> getGroupTags() {
194 final ArrayList<Tag> tags = new ArrayList<>();
195 for (final String group : getGroups(true)) {
196 tags.add(new Tag(group, UIHelper.getColorForName(group)));
197 }
198 return tags;
199 }
200
201 @Override
202 public List<Tag> getTags(Context context) {
203 final HashSet<Tag> tags = new HashSet<>();
204 tags.addAll(getGroupTags());
205 for (final String tag : getSystemTags(true)) {
206 tags.add(new Tag(tag, UIHelper.getColorForName(tag)));
207 }
208 Presence.Status status = getShownStatus();
209 if (status != Presence.Status.OFFLINE) {
210 tags.add(UIHelper.getTagForStatus(context, status));
211 }
212 if (isBlocked()) {
213 tags.add(new Tag(context.getString(R.string.blocked), 0xff2e2f3b));
214 }
215 if (!showInRoster() && getSystemAccount() != null) {
216 tags.add(new Tag("Android", UIHelper.getColorForName("Android")));
217 }
218 return new ArrayList<>(tags);
219 }
220
221 public boolean match(Context context, String needle) {
222 if (TextUtils.isEmpty(needle)) {
223 return true;
224 }
225 needle = needle.toLowerCase(Locale.US).trim();
226 String[] parts = needle.split("[,\\s]+");
227 if (parts.length > 1) {
228 for (String part : parts) {
229 if (!match(context, part)) {
230 return false;
231 }
232 }
233 return true;
234 } else if(parts.length > 0) {
235 return jid.toString().contains(parts[0]) ||
236 getDisplayName().toLowerCase(Locale.US).contains(parts[0]) ||
237 matchInTag(context, parts[0]);
238 } else {
239 return jid.toString().contains(needle) ||
240 getDisplayName().toLowerCase(Locale.US).contains(needle);
241 }
242 }
243
244 private boolean matchInTag(Context context, String needle) {
245 needle = needle.toLowerCase(Locale.US);
246 for (Tag tag : getTags(context)) {
247 if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
248 return true;
249 }
250 }
251 return false;
252 }
253
254 public ContentValues getContentValues() {
255 synchronized (this.keys) {
256 final ContentValues values = new ContentValues();
257 values.put(ACCOUNT, accountUuid);
258 values.put(SYSTEMNAME, systemName);
259 values.put(SERVERNAME, serverName);
260 values.put(PRESENCE_NAME, presenceName);
261 values.put(JID, jid.toString());
262 values.put(OPTIONS, subscription);
263 values.put(SYSTEMACCOUNT, systemAccount != null ? systemAccount.toString() : null);
264 values.put(PHOTOURI, photoUri);
265 values.put(KEYS, keys.toString());
266 values.put(AVATAR, avatar == null ? null : avatar.getFilename());
267 values.put(LAST_PRESENCE, mLastPresence);
268 values.put(LAST_TIME, mLastseen);
269 values.put(GROUPS, groups.toString());
270 values.put(RTP_CAPABILITY, rtpCapability == null ? null : rtpCapability.toString());
271 return values;
272 }
273 }
274
275 public Account getAccount() {
276 return this.account;
277 }
278
279 public void setAccount(Account account) {
280 this.account = account;
281 this.accountUuid = account.getUuid();
282 }
283
284 public Presences getPresences() {
285 return this.presences;
286 }
287
288 public void updatePresence(final String resource, final Presence presence) {
289 this.presences.updatePresence(resource, presence);
290 }
291
292 public void removePresence(final String resource) {
293 this.presences.removePresence(resource);
294 }
295
296 public void clearPresences() {
297 this.presences.clearPresences();
298 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
299 }
300
301 public Presence.Status getShownStatus() {
302 return this.presences.getShownStatus();
303 }
304
305 public Jid resourceWhichSupport(final String namespace) {
306 final String resource = getPresences().firstWhichSupport(namespace);
307 if (resource == null) return null;
308
309 return resource.equals("") ? getJid() : getJid().withResource(resource);
310 }
311
312 public boolean setPhotoUri(String uri) {
313 if (uri != null && !uri.equals(this.photoUri)) {
314 this.photoUri = uri;
315 return true;
316 } else if (this.photoUri != null && uri == null) {
317 this.photoUri = null;
318 return true;
319 } else {
320 return false;
321 }
322 }
323
324 public void setServerName(String serverName) {
325 this.serverName = serverName;
326 }
327
328 public boolean setSystemName(String systemName) {
329 final String old = getDisplayName();
330 this.systemName = systemName;
331 return !old.equals(getDisplayName());
332 }
333
334 public boolean setSystemTags(Collection<String> systemTags) {
335 final JSONArray old = this.systemTags;
336 this.systemTags = new JSONArray();
337 for(String tag : systemTags) {
338 this.systemTags.put(tag);
339 }
340 return !old.equals(this.systemTags);
341 }
342
343 public boolean setPresenceName(String presenceName) {
344 final String old = getDisplayName();
345 this.presenceName = presenceName;
346 return !old.equals(getDisplayName());
347 }
348
349 public Uri getSystemAccount() {
350 return systemAccount;
351 }
352
353 public void setSystemAccount(Uri lookupUri) {
354 this.systemAccount = lookupUri;
355 }
356
357 public void setGroups(List<String> groups) {
358 this.groups = new JSONArray(groups);
359 }
360
361 private Collection<String> getGroups(final boolean unique) {
362 final Collection<String> groups = unique ? new HashSet<>() : new ArrayList<>();
363 for (int i = 0; i < this.groups.length(); ++i) {
364 try {
365 groups.add(this.groups.getString(i));
366 } catch (final JSONException ignored) {
367 }
368 }
369 return groups;
370 }
371
372 public void copySystemTagsToGroups() {
373 for (String tag : getSystemTags(true)) {
374 this.groups.put(tag);
375 }
376 }
377
378 private Collection<String> getSystemTags(final boolean unique) {
379 final Collection<String> tags = unique ? new HashSet<>() : new ArrayList<>();
380 for (int i = 0; i < this.systemTags.length(); ++i) {
381 try {
382 tags.add(this.systemTags.getString(i));
383 } catch (final JSONException ignored) {
384 }
385 }
386 return tags;
387 }
388
389 public long getPgpKeyId() {
390 synchronized (this.keys) {
391 if (this.keys.has("pgp_keyid")) {
392 try {
393 return this.keys.getLong("pgp_keyid");
394 } catch (JSONException e) {
395 return 0;
396 }
397 } else {
398 return 0;
399 }
400 }
401 }
402
403 public boolean setPgpKeyId(long keyId) {
404 final long previousKeyId = getPgpKeyId();
405 synchronized (this.keys) {
406 try {
407 this.keys.put("pgp_keyid", keyId);
408 return previousKeyId != keyId;
409 } catch (final JSONException ignored) {
410 }
411 }
412 return false;
413 }
414
415 public void setOption(int option) {
416 this.subscription |= 1 << option;
417 }
418
419 public void resetOption(int option) {
420 this.subscription &= ~(1 << option);
421 }
422
423 public boolean getOption(int option) {
424 return ((this.subscription & (1 << option)) != 0);
425 }
426
427 public boolean canInferPresence() {
428 return showInContactList() || isSelf();
429 }
430
431 public boolean showInRoster() {
432 return (this.getOption(Contact.Options.IN_ROSTER) && (!this
433 .getOption(Contact.Options.DIRTY_DELETE)))
434 || (this.getOption(Contact.Options.DIRTY_PUSH));
435 }
436
437 public boolean showInContactList() {
438 return showInRoster()
439 || getOption(Options.SYNCED_VIA_OTHER)
440 || (QuickConversationsService.isQuicksy() && systemAccount != null);
441 }
442
443 public void parseSubscriptionFromElement(Element item) {
444 String ask = item.getAttribute("ask");
445 String subscription = item.getAttribute("subscription");
446
447 if (subscription == null) {
448 this.resetOption(Options.FROM);
449 this.resetOption(Options.TO);
450 } else {
451 switch (subscription) {
452 case "to":
453 this.resetOption(Options.FROM);
454 this.setOption(Options.TO);
455 break;
456 case "from":
457 this.resetOption(Options.TO);
458 this.setOption(Options.FROM);
459 this.resetOption(Options.PREEMPTIVE_GRANT);
460 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
461 break;
462 case "both":
463 this.setOption(Options.TO);
464 this.setOption(Options.FROM);
465 this.resetOption(Options.PREEMPTIVE_GRANT);
466 this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
467 break;
468 case "none":
469 this.resetOption(Options.FROM);
470 this.resetOption(Options.TO);
471 break;
472 }
473 }
474
475 // do NOT override asking if pending push request
476 if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
477 if ((ask != null) && (ask.equals("subscribe"))) {
478 this.setOption(Contact.Options.ASKING);
479 } else {
480 this.resetOption(Contact.Options.ASKING);
481 }
482 }
483 }
484
485 public void parseGroupsFromElement(Element item) {
486 this.groups = new JSONArray();
487 for (Element element : item.getChildren()) {
488 if (element.getName().equals("group") && element.getContent() != null) {
489 this.groups.put(element.getContent());
490 }
491 }
492 }
493
494 public Element asElement() {
495 final Element item = new Element("item");
496 item.setAttribute("jid", this.jid);
497 if (this.serverName != null) {
498 item.setAttribute("name", this.serverName);
499 } else {
500 item.setAttribute("name", getDisplayName());
501 }
502 for (String group : getGroups(false)) {
503 item.addChild("group").setContent(group);
504 }
505 return item;
506 }
507
508 @Override
509 public int compareTo(@NonNull final ListItem another) {
510 if (getJid().isDomainJid() && !another.getJid().isDomainJid()) {
511 return -1;
512 } else if (!getJid().isDomainJid() && another.getJid().isDomainJid()) {
513 return 1;
514 }
515
516 return this.getDisplayName().compareToIgnoreCase(
517 another.getDisplayName());
518 }
519
520 public String getServer() {
521 return getJid().getDomain().toEscapedString();
522 }
523
524 public void setAvatar(Avatar avatar) {
525 setAvatar(avatar, false);
526 }
527
528 public void setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) {
529 if (this.avatar != null && this.avatar.equals(avatar)) {
530 return;
531 }
532 if (!previouslyOmittedPepFetch && this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
533 return;
534 }
535 this.avatar = avatar;
536 }
537
538 public String getAvatarFilename() {
539 return avatar == null ? null : avatar.getFilename();
540 }
541
542 public Avatar getAvatar() {
543 return avatar;
544 }
545
546 public boolean mutualPresenceSubscription() {
547 return getOption(Options.FROM) && getOption(Options.TO);
548 }
549
550 @Override
551 public boolean isBlocked() {
552 return getAccount().isBlocked(this);
553 }
554
555 @Override
556 public boolean isDomainBlocked() {
557 return getAccount().isBlocked(this.getJid().getDomain());
558 }
559
560 @Override
561 public Jid getBlockedJid() {
562 if (isDomainBlocked()) {
563 return getJid().getDomain();
564 } else {
565 return getJid();
566 }
567 }
568
569 public boolean isSelf() {
570 return account.getJid().asBareJid().equals(jid.asBareJid());
571 }
572
573 boolean isOwnServer() {
574 return account.getJid().getDomain().equals(jid.asBareJid());
575 }
576
577 public void setCommonName(String cn) {
578 this.commonName = cn;
579 }
580
581 public void flagActive() {
582 this.mActive = true;
583 }
584
585 public void flagInactive() {
586 this.mActive = false;
587 }
588
589 public boolean isActive() {
590 return this.mActive;
591 }
592
593 public boolean setLastseen(long timestamp) {
594 if (timestamp > this.mLastseen) {
595 this.mLastseen = timestamp;
596 return true;
597 } else {
598 return false;
599 }
600 }
601
602 public long getLastseen() {
603 return this.mLastseen;
604 }
605
606 public void setLastResource(String resource) {
607 this.mLastPresence = resource;
608 }
609
610 public String getLastResource() {
611 return this.mLastPresence;
612 }
613
614 public String getServerName() {
615 return serverName;
616 }
617
618 public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
619 setOption(getOption(phoneContact.getClass()));
620 setSystemAccount(phoneContact.getLookupUri());
621 boolean changed = setSystemName(phoneContact.getDisplayName());
622 changed |= setPhotoUri(phoneContact.getPhotoUri());
623 return changed;
624 }
625
626 public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
627 resetOption(getOption(clazz));
628 boolean changed = false;
629 if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
630 setSystemAccount(null);
631 changed |= setPhotoUri(null);
632 changed |= setSystemName(null);
633 }
634 return changed;
635 }
636
637 protected String phoneAccountLabel() {
638 return account.getJid().asBareJid().toString() +
639 "/" + getJid().asBareJid().toString();
640 }
641
642 public PhoneAccountHandle phoneAccountHandle() {
643 ComponentName componentName = new ComponentName(
644 "com.cheogram.android",
645 "com.cheogram.android.ConnectionService"
646 );
647 return new PhoneAccountHandle(componentName, phoneAccountLabel());
648 }
649
650 // This Contact is a gateway to use for voice calls, register it with OS
651 public void registerAsPhoneAccount(XmppConnectionService ctx) {
652 if (Build.VERSION.SDK_INT < 23) return;
653 if (Build.VERSION.SDK_INT >= 33) {
654 if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM)) return;
655 } else {
656 if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
657 }
658
659 TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
660
661 PhoneAccount phoneAccount = PhoneAccount.builder(
662 phoneAccountHandle(),
663 account.getJid().asBareJid().toString()
664 ).setAddress(
665 Uri.fromParts("xmpp", account.getJid().asBareJid().toString(), null)
666 ).setIcon(
667 Icon.createWithBitmap(ctx.getAvatarService().get(this, AvatarService.getSystemUiAvatarSize(ctx) / 2, false))
668 ).setHighlightColor(
669 0x7401CF
670 ).setShortDescription(
671 getJid().asBareJid().toString()
672 ).setCapabilities(
673 PhoneAccount.CAPABILITY_CALL_PROVIDER
674 ).build();
675
676 telecomManager.registerPhoneAccount(phoneAccount);
677 }
678
679 // Unregister any associated PSTN gateway integration
680 public void unregisterAsPhoneAccount(Context ctx) {
681 if (Build.VERSION.SDK_INT < 23) return;
682 if (Build.VERSION.SDK_INT >= 33) {
683 if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM)) return;
684 } else {
685 if (!ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return;
686 }
687
688 TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
689 telecomManager.unregisterPhoneAccount(phoneAccountHandle());
690 }
691
692 public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
693 if (clazz == JabberIdContact.class) {
694 return Options.SYNCED_VIA_ADDRESSBOOK;
695 } else {
696 return Options.SYNCED_VIA_OTHER;
697 }
698 }
699
700 @Override
701 public int getAvatarBackgroundColor() {
702 return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
703 }
704
705 @Override
706 public String getAvatarName() {
707 return getDisplayName();
708 }
709
710 public boolean hasAvatarOrPresenceName() {
711 return (avatar != null && avatar.getFilename() != null) || presenceName != null;
712 }
713
714 public boolean refreshRtpCapability() {
715 final RtpCapability.Capability previous = this.rtpCapability;
716 this.rtpCapability = RtpCapability.check(this, false);
717 return !Objects.equals(previous, this.rtpCapability);
718 }
719
720 public RtpCapability.Capability getRtpCapability() {
721 return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability;
722 }
723
724 public static final class Options {
725 public static final int TO = 0;
726 public static final int FROM = 1;
727 public static final int ASKING = 2;
728 public static final int PREEMPTIVE_GRANT = 3;
729 public static final int IN_ROSTER = 4;
730 public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
731 public static final int DIRTY_PUSH = 6;
732 public static final int DIRTY_DELETE = 7;
733 private static final int SYNCED_VIA_ADDRESSBOOK = 8;
734 public static final int SYNCED_VIA_OTHER = 9;
735 }
736}