Merge lp:~tiagosh/telephony-service/use-libphonenumber into lp:telephony-service

Proposed by Tiago Salem Herrmann on 2015-07-15
Status: Merged
Approved by: Gustavo Pichorim Boiko on 2015-08-11
Approved revision: 1115
Merged at revision: 1110
Proposed branch: lp:~tiagosh/telephony-service/use-libphonenumber
Merge into: lp:telephony-service
Diff against target: 1022 lines (+254/-371)
19 files modified
Ubuntu/Telephony/contactwatcher.cpp (+1/-1)
debian/copyright (+0/-17)
indicator/textchannelobserver.cpp (+1/-1)
libtelephonyservice/CMakeLists.txt (+2/-0)
libtelephonyservice/multimediaaccountentry.cpp (+1/-1)
libtelephonyservice/ofonoaccountentry.cpp (+27/-1)
libtelephonyservice/ofonoaccountentry.h (+5/-0)
libtelephonyservice/phonenumberutils.h (+0/-303)
libtelephonyservice/phoneutils.cpp (+100/-24)
libtelephonyservice/phoneutils.h (+20/-2)
tests/common/mock/MockConnection.xml (+6/-0)
tests/common/mock/connection.cpp (+7/-0)
tests/common/mock/connection.h (+2/-0)
tests/common/mock/emergencymodeiface.cpp (+28/-1)
tests/common/mock/emergencymodeiface.h (+12/-0)
tests/common/mock/mockconnectiondbus.cpp (+6/-0)
tests/common/mock/mockconnectiondbus.h (+1/-0)
tests/libtelephonyservice/OfonoAccountEntryTest.cpp (+18/-0)
tests/libtelephonyservice/PhoneUtilsTest.cpp (+17/-20)
To merge this branch: bzr merge lp:~tiagosh/telephony-service/use-libphonenumber
Reviewer Review Type Date Requested Status
Gustavo Pichorim Boiko (community) 2015-07-15 Approve on 2015-08-10
PS Jenkins bot continuous-integration Approve on 2015-08-10
Review via email: mp+264906@code.launchpad.net

Commit Message

Use libphonenumber for phone number validation, normalization and comparison.

Description of the Change

Use libphonenumber for phone number validation, normalization and comparison.

--Checklist--
Are there any related MPs required for this MP to build/function as expected? Please list.
No

Is your branch in sync with latest trunk (e.g. bzr pull lp:trunk -> no changes)
Yes

Did you perform an exploratory manual test run of your code change and any related functionality on device or emulator?
Yes

Did you successfully run all tests found in your component's Test Plan (https://wiki.ubuntu.com/Process/Merges/TestPlan/<package-name>) on device or emulator?
Yes

If you changed the UI, was the change specified/approved by design?
N/A

If you changed UI labels, did you update the pot file?
N/A

If you changed the packaging (debian), did you add a core-dev as a reviewer to this MP?
N/A

To post a comment you must log in.
1103. By Tiago Salem Herrmann on 2015-07-17

reenable tests

1104. By Tiago Salem Herrmann on 2015-07-17

fix copyright header

1105. By Tiago Salem Herrmann on 2015-07-17

revert po changes

1106. By Tiago Salem Herrmann on 2015-07-17

Provide a fallback to country code if none is provided

1107. By Tiago Salem Herrmann on 2015-07-20

Read countryCode() from ofono

1108. By Tiago Salem Herrmann on 2015-07-20

Add code to fallback

1109. By Tiago Salem Herrmann on 2015-07-21

Use IsEmergencyNumber() instead of ConnectsToEmergencyNumber()

1110. By Tiago Salem Herrmann on 2015-07-23

Change comparePhoneNumbers() to return PhoneUtils::PhoneNumberMatchType

1111. By Tiago Salem Herrmann on 2015-07-23

merge trunk

1112. By Tiago Salem Herrmann on 2015-07-31

Only use libphonenumber if both numbers have more than 7 digits

1113. By Tiago Salem Herrmann on 2015-08-05

fix build with libphonenumber >= 7

Gustavo Pichorim Boiko (boiko) wrote :

Just one request, other than that the code looks good!

review: Needs Fixing
1114. By Tiago Salem Herrmann on 2015-08-10

add test for numbers with slash

1115. By Tiago Salem Herrmann on 2015-08-10

add test for phones with slashes

Gustavo Pichorim Boiko (boiko) wrote :

Did you perform an exploratory manual test run of the code change and any related functionality on device or emulator?
Yes

Did CI run pass? If not, please explain why.
Yes

Have you checked that submitter has accurately filled out the submitter checklist and has taken no shortcut?
Yes

Code looks good and works as expected!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Ubuntu/Telephony/contactwatcher.cpp'
2--- Ubuntu/Telephony/contactwatcher.cpp 2015-07-15 18:56:53 +0000
3+++ Ubuntu/Telephony/contactwatcher.cpp 2015-08-10 22:00:00 +0000
4@@ -301,7 +301,7 @@
5 Q_FOREACH(const QString &field, mAddressableFields) {
6 if (field == "tel") {
7 Q_FOREACH(const QContactPhoneNumber phoneNumber, contact.details(QContactDetail::TypePhoneNumber)) {
8- if (PhoneUtils::comparePhoneNumbers(phoneNumber.number(), mIdentifier)) {
9+ if (PhoneUtils::comparePhoneNumbers(phoneNumber.number(), mIdentifier) > PhoneUtils::NO_MATCH) {
10 mDetailProperties["type"] = (int)QContactDetail::TypePhoneNumber;
11 mDetailProperties["phoneNumberSubTypes"] = wrapIntList(phoneNumber.subTypes());
12 mDetailProperties["phoneNumberContexts"] = wrapIntList(phoneNumber.contexts());
13
14=== modified file 'debian/copyright'
15--- debian/copyright 2013-08-26 10:32:50 +0000
16+++ debian/copyright 2015-08-10 22:00:00 +0000
17@@ -6,10 +6,6 @@
18 Copyright: 2012-2013 Canonical Ltd.
19 License: GPL-3
20
21-Files: libtelephonyservice/phonenumberutils.h
22-Copyright: 2006 The Android Open Source Project
23-License: Apache-2
24-
25 Files: assets/*
26 icons/*
27 Copyright: 2012-2013 Canonical Ltd.
28@@ -31,19 +27,6 @@
29 On Debian systems, the full text of the GNU General Public License
30 version 3 can be found in the file /usr/share/common-licenses/GPL-3.
31
32-License: Apache-2
33- Licensed under the Apache License, Version 2.0 (the "License");
34- you may not use this file except in compliance with the License.
35- You may obtain a copy of the License at
36- .
37- http://www.apache.org/licenses/LICENSE-2.0
38- .
39- Unless required by applicable law or agreed to in writing, software
40- distributed under the License is distributed on an "AS IS" BASIS,
41- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
42- See the License for the specific language governing permissions and
43- limitations under the License.
44-
45 License: CC-BY-SA-3.0
46 This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
47 Unported License. To view a copy of this license, visit
48
49=== modified file 'indicator/textchannelobserver.cpp'
50--- indicator/textchannelobserver.cpp 2015-06-11 16:38:02 +0000
51+++ indicator/textchannelobserver.cpp 2015-08-10 22:00:00 +0000
52@@ -498,7 +498,7 @@
53
54 // FIXME: add support for contact matching for non phone number based accounts
55 Q_FOREACH(const QContactPhoneNumber phoneNumber, contact.details(QContactDetail::TypePhoneNumber)) {
56- if (PhoneUtils::comparePhoneNumbers(data->senderId, phoneNumber.number())) {
57+ if (PhoneUtils::comparePhoneNumbers(data->senderId, phoneNumber.number()) > PhoneUtils::NO_MATCH) {
58 QString displayLabel = contact.detail<QContactDisplayLabel>().label();
59 QString title = QString::fromUtf8(C::gettext("Message from %1")).arg(displayLabel.isEmpty() ? data->alias : displayLabel);
60 QString avatar = contact.detail<QContactAvatar>().imageUrl().toEncoded();
61
62=== modified file 'libtelephonyservice/CMakeLists.txt'
63--- libtelephonyservice/CMakeLists.txt 2015-06-11 16:48:18 +0000
64+++ libtelephonyservice/CMakeLists.txt 2015-08-10 22:00:00 +0000
65@@ -25,6 +25,7 @@
66 ${TP_QT5_INCLUDE_DIRS}
67 ${NOTIFY_INCLUDE_DIRS}
68 ${GSETTINGS_QT_INCLUDE_DIRS}
69+ ${LibPhoneNumber_INCLUDE_DIRS}
70 )
71
72 if (USE_UBUNTU_PLATFORM_API)
73@@ -37,6 +38,7 @@
74 ${TP_QT5_LIBRARIES}
75 ${UBUNTU_APP_LIB}
76 ${NOTIFY_LIBRARIES}
77+ ${LibPhoneNumber_LIBRARIES}
78 ${GSETTINGS_QT_LDFLAGS})
79
80 qt5_use_modules(telephonyservice Contacts Core DBus Feedback Multimedia Qml Quick)
81
82=== modified file 'libtelephonyservice/multimediaaccountentry.cpp'
83--- libtelephonyservice/multimediaaccountentry.cpp 2015-06-05 20:36:01 +0000
84+++ libtelephonyservice/multimediaaccountentry.cpp 2015-08-10 22:00:00 +0000
85@@ -43,7 +43,7 @@
86
87 bool MultimediaAccountEntry::compareIds(const QString &first, const QString &second) const
88 {
89- return PhoneUtils::comparePhoneNumbers(first, second);
90+ return PhoneUtils::comparePhoneNumbers(first, second) > PhoneUtils::NO_MATCH;
91 }
92
93 QStringList MultimediaAccountEntry::addressableVCardFields()
94
95=== modified file 'libtelephonyservice/ofonoaccountentry.cpp'
96--- libtelephonyservice/ofonoaccountentry.cpp 2015-05-11 13:16:25 +0000
97+++ libtelephonyservice/ofonoaccountentry.cpp 2015-08-10 22:00:00 +0000
98@@ -51,6 +51,11 @@
99 return mEmergencyNumbers;
100 }
101
102+QString OfonoAccountEntry::countryCode() const
103+{
104+ return mCountryCode;
105+}
106+
107 QString OfonoAccountEntry::voicemailNumber() const
108 {
109 return mVoicemailNumber;
110@@ -111,7 +116,7 @@
111
112 bool OfonoAccountEntry::compareIds(const QString &first, const QString &second) const
113 {
114- return PhoneUtils::comparePhoneNumbers(first, second);
115+ return PhoneUtils::comparePhoneNumbers(first, second) > PhoneUtils::NO_MATCH;
116 }
117
118 QStringList OfonoAccountEntry::addressableVCardFields()
119@@ -125,6 +130,12 @@
120 Q_EMIT emergencyNumbersChanged();
121 }
122
123+void OfonoAccountEntry::onCountryCodeChanged(const QString &countryCode)
124+{
125+ mCountryCode = countryCode;
126+ Q_EMIT countryCodeChanged();
127+}
128+
129 void OfonoAccountEntry::onVoicemailNumberChanged(const QString &number)
130 {
131 mVoicemailNumber = number;
132@@ -156,6 +167,9 @@
133 dbusConnection.disconnect(mConnectionInfo.busName, mConnectionInfo.objectPath,
134 CANONICAL_TELEPHONY_EMERGENCYMODE_IFACE, "EmergencyNumbersChanged",
135 this, SLOT(onEmergencyNumbersChanged(QStringList)));
136+ dbusConnection.disconnect(mConnectionInfo.busName, mConnectionInfo.objectPath,
137+ CANONICAL_TELEPHONY_EMERGENCYMODE_IFACE, "CountryCodeChanged",
138+ this, SLOT(onCountryCodeChanged(QString)));
139 }
140 } else {
141 // connect the emergency numbers changed signal
142@@ -171,6 +185,18 @@
143 Q_EMIT emergencyNumbersChanged();
144 }
145
146+ // connect the country code changed signal
147+ dbusConnection.connect(mConnectionInfo.busName, mConnectionInfo.objectPath,
148+ CANONICAL_TELEPHONY_EMERGENCYMODE_IFACE, "CountryCodeChanged",
149+ this, SLOT(onCountryCodeChanged(QString)));
150+
151+ // and get the current value of the country code
152+ QDBusReply<QString> replyCountryCode = connIface.call("CountryCode");
153+ if (replyCountryCode.isValid()) {
154+ mCountryCode = replyCountryCode.value();
155+ Q_EMIT countryCodeChanged();
156+ }
157+
158 // connect the voicemail number changed signal
159 dbusConnection.connect(mConnectionInfo.busName, mConnectionInfo.objectPath,
160 CANONICAL_TELEPHONY_VOICEMAIL_IFACE, "VoicemailNumberChanged",
161
162=== modified file 'libtelephonyservice/ofonoaccountentry.h'
163--- libtelephonyservice/ofonoaccountentry.h 2015-04-16 22:26:23 +0000
164+++ libtelephonyservice/ofonoaccountentry.h 2015-08-10 22:00:00 +0000
165@@ -36,6 +36,7 @@
166 Q_PROPERTY(bool emergencyCallsAvailable READ emergencyCallsAvailable NOTIFY emergencyCallsAvailableChanged)
167 Q_PROPERTY(bool simLocked READ simLocked NOTIFY simLockedChanged)
168 Q_PROPERTY(QString serial READ serial NOTIFY serialChanged)
169+ Q_PROPERTY(QString countryCode READ countryCode NOTIFY countryCodeChanged)
170 Q_PROPERTY(USSDManager* ussdManager READ ussdManager CONSTANT)
171 friend class AccountEntryFactory;
172
173@@ -45,6 +46,7 @@
174 uint voicemailCount() const;
175 bool voicemailIndicator() const;
176 QString networkName() const;
177+ QString countryCode() const;
178 bool emergencyCallsAvailable() const;
179 bool simLocked() const;
180 QString serial() const;
181@@ -62,12 +64,14 @@
182 void voicemailCountChanged();
183 void voicemailIndicatorChanged();
184 void networkNameChanged();
185+ void countryCodeChanged();
186 void emergencyCallsAvailableChanged();
187 void simLockedChanged();
188 void serialChanged();
189
190 private Q_SLOTS:
191 void onEmergencyNumbersChanged(const QStringList &numbers);
192+ void onCountryCodeChanged(const QString &countryCode);
193 void onVoicemailNumberChanged(const QString &number);
194 void onVoicemailCountChanged(uint count);
195 void onVoicemailIndicatorChanged(bool visible);
196@@ -80,6 +84,7 @@
197
198 private:
199 QStringList mEmergencyNumbers;
200+ QString mCountryCode;
201 QString mVoicemailNumber;
202 uint mVoicemailCount;
203 bool mVoicemailIndicator;
204
205=== removed file 'libtelephonyservice/phonenumberutils.h'
206--- libtelephonyservice/phonenumberutils.h 2013-11-08 13:11:49 +0000
207+++ libtelephonyservice/phonenumberutils.h 1970-01-01 00:00:00 +0000
208@@ -1,303 +0,0 @@
209-/*
210- * Copyright (C) 2006 The Android Open Source Project
211- *
212- * Licensed under the Apache License, Version 2.0 (the "License");
213- * you may not use this file except in compliance with the License.
214- * You may obtain a copy of the License at
215- *
216- * http://www.apache.org/licenses/LICENSE-2.0
217- *
218- * Unless required by applicable law or agreed to in writing, software
219- * distributed under the License is distributed on an "AS IS" BASIS,
220- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
221- * See the License for the specific language governing permissions and
222- * limitations under the License.
223- *
224- * Original source code available at: http://androidxref.com/4.0.4/xref/frameworks/base/telephony/java/android/telephony/PhoneNumberUtils.java
225- */
226-
227-#ifndef PHONENUMBERUTILS_H
228-#define PHONENUMBERUTILS_H
229-
230-#include <QRegExp>
231-
232-namespace PhoneNumberUtils
233-{
234-
235-/** True if c is ISO-LATIN characters 0-9, *, # , +, WILD, WAIT, PAUSE */
236-bool isNonSeparator(char c)
237-{
238- return (c >= '0' && c <= '9') || c == '*' || c == '#' || c == '+'
239- || c == 'N' || c == ';' || c == ',';
240-}
241-
242-/** True if c is ISO-LATIN characters 0-9, *, # , +, WILD */
243-bool isDialable(char c)
244-{
245- return (c >= '0' && c <= '9') || c == '*' || c == '#' || c == '+' || c == 'N';
246-}
247-
248-/** True if c is ISO-LATIN characters 0-9 */
249-bool isISODigit (char c) {
250- return c >= '0' && c <= '9';
251-}
252-
253-/** or -1 if both are negative */
254-int minPositive (int a, int b)
255-{
256- if (a >= 0 && b >= 0) {
257- return (a < b) ? a : b;
258- } else if (a >= 0) { /* && b < 0 */
259- return a;
260- } else if (b >= 0) { /* && a < 0 */
261- return b;
262- } else { /* a < 0 && b < 0 */
263- return -1;
264- }
265-}
266-
267-/** index of the last character of the network portion
268- * (eg anything after is a post-dial string)
269- */
270-int indexOfLastNetworkChar(const QString &a)
271-{
272- int pIndex, wIndex;
273- int origLength;
274- int trimIndex;
275-
276- origLength = a.length();
277-
278- pIndex = a.indexOf(',');
279- wIndex = a.indexOf(';');
280-
281- trimIndex = minPositive(pIndex, wIndex);
282-
283- if (trimIndex < 0) {
284- return origLength - 1;
285- } else {
286- return trimIndex - 1;
287- }
288-}
289-
290-/** all of a up to len must be an international prefix or
291- * separators/non-dialing digits
292- */
293-bool matchIntlPrefix(const QString &a, int len)
294-{
295- /* '([^0-9*#+pwn]\+[^0-9*#+pwn] | [^0-9*#+pwn]0(0|11)[^0-9*#+pwn] )$' */
296- /* 0 1 2 3 45 */
297-
298- int state = 0;
299- for (int i = 0 ; i < len ; i++) {
300- char c = a.at(i).toLatin1();
301-
302- switch (state) {
303- case 0:
304- if (c == '+') state = 1;
305- else if (c == '0') state = 2;
306- else if (isNonSeparator(c)) return false;
307- break;
308-
309- case 2:
310- if (c == '0') state = 3;
311- else if (c == '1') state = 4;
312- else if (isNonSeparator(c)) return false;
313- break;
314-
315- case 4:
316- if (c == '1') state = 5;
317- else if (isNonSeparator(c)) return false;
318- break;
319-
320- default:
321- if (isNonSeparator(c)) return false;
322- break;
323-
324- }
325- }
326-
327- return state == 1 || state == 3 || state == 5;
328-}
329-
330-/** all of 'a' up to len must match non-US trunk prefix ('0') */
331-bool matchTrunkPrefix(const QString &a, int len) {
332- bool found;
333-
334- found = false;
335-
336- for (int i = 0 ; i < len ; i++) {
337- char c = a.at(i).toLatin1();
338-
339- if (c == '0' && !found) {
340- found = true;
341- } else if (isNonSeparator(c)) {
342- return false;
343- }
344- }
345-
346- return found;
347-}
348-
349-/** all of 'a' up to len must be a (+|00|011)country code)
350- * We're fast and loose with the country code. Any \d{1,3} matches */
351-bool matchIntlPrefixAndCC(const QString &a, int len) {
352- /* [^0-9*#+pwn]*(\+|0(0|11)\d\d?\d? [^0-9*#+pwn] $ */
353- /* 0 1 2 3 45 6 7 8 */
354-
355- int state = 0;
356- for (int i = 0 ; i < len ; i++ ) {
357- char c = a.at(i).toLatin1();
358-
359- switch (state) {
360- case 0:
361- if (c == '+') state = 1;
362- else if (c == '0') state = 2;
363- else if (isNonSeparator(c)) return false;
364- break;
365-
366- case 2:
367- if (c == '0') state = 3;
368- else if (c == '1') state = 4;
369- else if (isNonSeparator(c)) return false;
370- break;
371-
372- case 4:
373- if (c == '1') state = 5;
374- else if (isNonSeparator(c)) return false;
375- break;
376-
377- case 1:
378- case 3:
379- case 5:
380- if (isISODigit(c)) state = 6;
381- else if (isNonSeparator(c)) return false;
382- break;
383-
384- case 6:
385- case 7:
386- if (isISODigit(c)) state++;
387- else if (isNonSeparator(c)) return false;
388- break;
389-
390- default:
391- if (isNonSeparator(c)) return false;
392- }
393- }
394-
395- return state == 6 || state == 7 || state == 8;
396-}
397-
398-
399-/**
400- * Compare phone numbers a and b, return true if they're identical
401- * enough for caller ID purposes.
402- *
403- * - Compares from right to left
404- * - requires MIN_MATCH (7) characters to match
405- * - handles common trunk prefixes and international prefixes
406- * (basically, everything except the Russian trunk prefix)
407- *
408- * Note that this method does not return false even when the two phone numbers
409- * are not exactly same; rather; we can call this method "similar()", not "equals()".
410- *
411- * @hide
412- */
413-bool compareLoosely(const QString &a, const QString &b)
414-{
415- int ia, ib;
416- int matched;
417- int numNonDialableCharsInA = 0;
418- int numNonDialableCharsInB = 0;
419-
420- if (a.length() == 0 || b.length() == 0) {
421- return false;
422- }
423-
424- if (a == b) {
425- return true;
426- }
427-
428- ia = indexOfLastNetworkChar (a);
429- ib = indexOfLastNetworkChar (b);
430- matched = 0;
431-
432- while (ia >= 0 && ib >=0) {
433- char ca, cb;
434- bool skipCmp = false;
435-
436- ca = a.at(ia).toLatin1();
437-
438- if (!isDialable(ca)) {
439- ia--;
440- skipCmp = true;
441- numNonDialableCharsInA++;
442- }
443-
444- cb = b.at(ib).toLatin1();
445-
446- if (!isDialable(cb)) {
447- ib--;
448- skipCmp = true;
449- numNonDialableCharsInB++;
450- }
451-
452- if (!skipCmp) {
453- if (cb != ca && ca != 'N' && cb != 'N') {
454- break;
455- }
456- ia--; ib--; matched++;
457- }
458- }
459-
460- if (matched < 7) {
461- int effectiveALen = a.length() - numNonDialableCharsInA;
462- int effectiveBLen = b.length() - numNonDialableCharsInB;
463-
464-
465- // if the number of dialable chars in a and b match, but the matched chars < MIN_MATCH,
466- // treat them as equal (i.e. 404-04 and 40404)
467- if (effectiveALen == effectiveBLen && effectiveALen == matched) {
468- return true;
469- }
470-
471- return false;
472- }
473-
474- // At least one string has matched completely;
475- if (matched >= 7 && (ia < 0 || ib < 0)) {
476- return true;
477- }
478-
479- /*
480- * Now, what remains must be one of the following for a
481- * match:
482- *
483- * - a '+' on one and a '00' or a '011' on the other
484- * - a '0' on one and a (+,00)<country code> on the other
485- * (for this, a '0' and a '00' prefix would have succeeded above)
486- */
487-
488- if (matchIntlPrefix(a, ia + 1)
489- && matchIntlPrefix (b, ib +1)
490- ) {
491- return true;
492- }
493-
494- if (matchTrunkPrefix(a, ia + 1)
495- && matchIntlPrefixAndCC(b, ib +1)
496- ) {
497- return true;
498- }
499-
500- if (matchTrunkPrefix(b, ib + 1)
501- && matchIntlPrefixAndCC(a, ia +1)
502- ) {
503- return true;
504- }
505-
506- return false;
507-}
508-
509-}
510-
511-#endif
512
513=== modified file 'libtelephonyservice/phoneutils.cpp'
514--- libtelephonyservice/phoneutils.cpp 2015-06-17 19:31:32 +0000
515+++ libtelephonyservice/phoneutils.cpp 2015-08-10 22:00:00 +0000
516@@ -1,8 +1,10 @@
517 /*
518- * Copyright (C) 2012 Canonical, Ltd.
519+ * Copyright (C) 2012-2015 Canonical, Ltd.
520 *
521 * Authors:
522 * Gustavo Pichorim Boiko <gustavo.boiko@canonical.com>
523+ * Renato Araujo Oliveira Filho <renato.filho@canonical.com>
524+ * Tiago Salem Herrmann <tiago.herrmann@canonical.com>
525 *
526 * This file is part of telephony-service.
527 *
528@@ -20,32 +22,106 @@
529 */
530
531 #include "phoneutils.h"
532-#include "phonenumberutils.h"
533+
534+#include <phonenumbers/phonenumbermatch.h>
535+#include <phonenumbers/phonenumbermatcher.h>
536+#include <phonenumbers/phonenumberutil.h>
537+#include <phonenumbers/shortnumberinfo.h>
538+
539+#include <QLocale>
540+#include <QDebug>
541+
542+QString PhoneUtils::mCountryCode = QString();
543
544 PhoneUtils::PhoneUtils(QObject *parent) :
545 QObject(parent)
546 {
547 }
548
549-bool PhoneUtils::comparePhoneNumbers(const QString &number1, const QString &number2)
550-{
551- if (isPhoneNumber(number1) && isPhoneNumber(number2)) {
552- return PhoneNumberUtils::compareLoosely(number1, number2);
553- }
554-
555- // if at least one of the id's is not a phone number, then perform a simple string comparison
556- return number1 == number2;
557-}
558-
559-bool PhoneUtils::isPhoneNumber(const QString &identifier) {
560- // remove all non diable digits
561- QString finalNumber = QString(identifier).replace(QRegExp("[p+*#/(),;-]"),"");
562- finalNumber = finalNumber.replace(QRegExp("(\\s+)"), "");
563- // if empty, the number is invalid
564- if (finalNumber.isEmpty())
565- return false;
566-
567- finalNumber = finalNumber.replace(QRegExp("(\\d+)"), "");
568- return finalNumber.isEmpty();
569-}
570-
571+void PhoneUtils::setCountryCode(const QString &countryCode)
572+{
573+ mCountryCode = countryCode;
574+}
575+
576+QString PhoneUtils::countryCode()
577+{
578+ if (!mCountryCode.isEmpty()) {
579+ return mCountryCode;
580+ }
581+
582+ QString countryCode = QLocale::system().name().split("_").last();
583+ if (countryCode.size() < 2) {
584+ // fallback to US if no valid country code was provided, otherwise libphonenumber
585+ // will fail to parse any numbers
586+ return QString("US");
587+ }
588+ return countryCode;
589+}
590+
591+QString PhoneUtils::normalizePhoneNumber(const QString &phoneNumber)
592+{
593+ static i18n::phonenumbers::PhoneNumberUtil *phonenumberUtil = i18n::phonenumbers::PhoneNumberUtil::GetInstance();
594+ if (!isPhoneNumber(phoneNumber)) {
595+ return phoneNumber;
596+ }
597+ std::string number = phoneNumber.toStdString();
598+ phonenumberUtil->NormalizeDiallableCharsOnly(&number);
599+ return QString::fromStdString(number);
600+}
601+
602+PhoneUtils::PhoneNumberMatchType PhoneUtils::comparePhoneNumbers(const QString &phoneNumberA, const QString &phoneNumberB)
603+{
604+ static i18n::phonenumbers::PhoneNumberUtil *phonenumberUtil = i18n::phonenumbers::PhoneNumberUtil::GetInstance();
605+
606+ // just do a simple string comparison if we are dealing with non phone numbers
607+ if (!isPhoneNumber(phoneNumberA) || !isPhoneNumber(phoneNumberB)) {
608+ return phoneNumberA == phoneNumberB ? PhoneUtils::EXACT_MATCH : PhoneUtils::INVALID_NUMBER;
609+ }
610+ QString normalizedPhoneNumberA = normalizePhoneNumber(phoneNumberA);
611+ QString normalizedPhoneNumberB = normalizePhoneNumber(phoneNumberB);
612+
613+ if (normalizedPhoneNumberA.size() < 7 || normalizedPhoneNumberB.size() < 7) {
614+ return normalizedPhoneNumberA == normalizedPhoneNumberB ? PhoneUtils::EXACT_MATCH : PhoneUtils::NO_MATCH;
615+ }
616+
617+ i18n::phonenumbers::PhoneNumberUtil::MatchType match = phonenumberUtil->
618+ IsNumberMatchWithTwoStrings(phoneNumberA.toStdString(),
619+ phoneNumberB.toStdString());
620+ return (PhoneNumberMatchType)match;
621+}
622+
623+bool PhoneUtils::isPhoneNumber(const QString &phoneNumber)
624+{
625+ static i18n::phonenumbers::PhoneNumberUtil *phonenumberUtil = i18n::phonenumbers::PhoneNumberUtil::GetInstance();
626+ std::string formattedNumber;
627+ i18n::phonenumbers::PhoneNumber number;
628+ i18n::phonenumbers::PhoneNumberUtil::ErrorType error;
629+ error = phonenumberUtil->Parse(phoneNumber.toStdString(), countryCode().toStdString(), &number);
630+
631+ switch(error) {
632+ case i18n::phonenumbers::PhoneNumberUtil::INVALID_COUNTRY_CODE_ERROR:
633+ qWarning() << "Invalid country code for:" << phoneNumber;
634+ return false;
635+ case i18n::phonenumbers::PhoneNumberUtil::NOT_A_NUMBER:
636+ qWarning() << "The phone number is not a valid number:" << phoneNumber;
637+ return false;
638+ case i18n::phonenumbers::PhoneNumberUtil::TOO_SHORT_AFTER_IDD:
639+ case i18n::phonenumbers::PhoneNumberUtil::TOO_SHORT_NSN:
640+ case i18n::phonenumbers::PhoneNumberUtil::TOO_LONG_NSN:
641+ qWarning() << "Invalid phone number" << phoneNumber;
642+ return false;
643+ default:
644+ break;
645+ }
646+ return true;
647+}
648+
649+bool PhoneUtils::isEmergencyNumber(const QString &phoneNumber, const QString &countryCode)
650+{
651+ QString finalCode = countryCode;
652+ if (finalCode.isEmpty()) {
653+ finalCode = PhoneUtils::countryCode();
654+ }
655+ static const i18n::phonenumbers::ShortNumberInfo short_info;
656+ return short_info.IsEmergencyNumber(normalizePhoneNumber(phoneNumber).toStdString(), finalCode.toStdString());
657+}
658
659=== modified file 'libtelephonyservice/phoneutils.h'
660--- libtelephonyservice/phoneutils.h 2013-11-11 14:54:50 +0000
661+++ libtelephonyservice/phoneutils.h 2015-08-10 22:00:00 +0000
662@@ -3,6 +3,8 @@
663 *
664 * Authors:
665 * Gustavo Pichorim Boiko <gustavo.boiko@canonical.com>
666+ * Renato Araujo Oliveira Filho <renato.filho@canonical.com>
667+ * Tiago Salem Herrmann <tiago.herrmann@canonical.com>
668 *
669 * This file is part of telephony-service.
670 *
671@@ -22,15 +24,31 @@
672 #ifndef PHONEUTILS_H
673 #define PHONEUTILS_H
674
675+#include <phonenumbers/phonenumberutil.h>
676 #include <QObject>
677
678 class PhoneUtils : public QObject
679 {
680 Q_OBJECT
681+ Q_ENUMS(PhoneNumberMatchType)
682+
683 public:
684+ enum PhoneNumberMatchType {
685+ INVALID_NUMBER = i18n::phonenumbers::PhoneNumberUtil::INVALID_NUMBER,
686+ NO_MATCH = i18n::phonenumbers::PhoneNumberUtil::NO_MATCH,
687+ SHORT_NSN_MATCH = i18n::phonenumbers::PhoneNumberUtil::SHORT_NSN_MATCH,
688+ NSN_MATCH = i18n::phonenumbers::PhoneNumberUtil::NSN_MATCH,
689+ EXACT_MATCH = i18n::phonenumbers::PhoneNumberUtil::EXACT_MATCH
690+ };
691 explicit PhoneUtils(QObject *parent = 0);
692- Q_INVOKABLE static bool comparePhoneNumbers(const QString &number1, const QString &number2);
693- Q_INVOKABLE static bool isPhoneNumber(const QString &identifier);
694+ Q_INVOKABLE static void setCountryCode(const QString &countryCode);
695+ Q_INVOKABLE static QString countryCode();
696+ Q_INVOKABLE static PhoneNumberMatchType comparePhoneNumbers(const QString &number1, const QString &number2);
697+ Q_INVOKABLE static bool isPhoneNumber(const QString &phoneNumber);
698+ Q_INVOKABLE static QString normalizePhoneNumber(const QString &phoneNumber);
699+ Q_INVOKABLE static bool isEmergencyNumber(const QString &phoneNumber, const QString &countryCode = QString());
700+private:
701+ static QString mCountryCode;
702
703 };
704
705
706=== modified file 'tests/common/mock/MockConnection.xml'
707--- tests/common/mock/MockConnection.xml 2015-04-17 19:52:20 +0000
708+++ tests/common/mock/MockConnection.xml 2015-08-10 22:00:00 +0000
709@@ -75,6 +75,12 @@
710 ]]></dox:d>
711 <arg name="numbers" type="as" direction="in"/>
712 </method>
713+ <method name="SetCountryCode">
714+ <dox:d><![CDATA[
715+ Set country code
716+ ]]></dox:d>
717+ <arg name="countrCode" type="s" direction="in"/>
718+ </method>
719 <method name="Serial">
720 <dox:d><![CDATA[
721 Get the USSD serial
722
723=== modified file 'tests/common/mock/connection.cpp'
724--- tests/common/mock/connection.cpp 2015-06-16 15:59:34 +0000
725+++ tests/common/mock/connection.cpp 2015-08-10 22:00:00 +0000
726@@ -127,6 +127,7 @@
727 plugInterface(Tp::AbstractConnectionInterfacePtr::dynamicCast(emergencyModeIface));
728 mEmergencyNumbers << "123" << "456" << "789";
729 emergencyModeIface->setEmergencyNumbers(mEmergencyNumbers);
730+ emergencyModeIface->setCountryCode("US");
731
732 // init custom voicemail interface (not provided by telepathy)
733 voicemailIface = BaseConnectionVoicemailInterface::create();
734@@ -595,6 +596,12 @@
735 emergencyModeIface->setEmergencyNumbers(emergencyNumbers);
736 }
737
738+void MockConnection::setCountryCode(const QString &countryCode)
739+{
740+ mCountryCode = countryCode;
741+ emergencyModeIface->setCountryCode(countryCode);
742+}
743+
744 bool MockConnection::voicemailIndicator(Tp::DBusError *error)
745 {
746 return mVoicemailIndicator;
747
748=== modified file 'tests/common/mock/connection.h'
749--- tests/common/mock/connection.h 2015-06-16 15:59:34 +0000
750+++ tests/common/mock/connection.h 2015-08-10 22:00:00 +0000
751@@ -89,6 +89,7 @@
752
753 QStringList emergencyNumbers(Tp::DBusError *error);
754 void setEmergencyNumbers(const QStringList &emergencyNumbers);
755+ void setCountryCode(const QString &countryCode);
756
757 bool voicemailIndicator(Tp::DBusError *error);
758 void setVoicemailIndicator(bool visible);
759@@ -155,6 +156,7 @@
760 MockConferenceCallChannel *mConferenceCall;
761
762 QStringList mEmergencyNumbers;
763+ QString mCountryCode;
764 int mVoicemailCount;
765 bool mVoicemailIndicator;
766 QString mVoicemailNumber;
767
768=== modified file 'tests/common/mock/emergencymodeiface.cpp'
769--- tests/common/mock/emergencymodeiface.cpp 2015-03-11 17:02:35 +0000
770+++ tests/common/mock/emergencymodeiface.cpp 2015-08-10 22:00:00 +0000
771@@ -38,6 +38,7 @@
772 EmergencyNumbersCallback emergencyNumbersCB;
773 BaseConnectionEmergencyModeInterface::Adaptee *adaptee;
774 QString fakeEmergencyNumber;
775+ QString countryCode;
776 };
777
778 BaseConnectionEmergencyModeInterface::Adaptee::~Adaptee()
779@@ -63,6 +64,11 @@
780 }
781 }
782
783+void BaseConnectionEmergencyModeInterface::Adaptee::countryCode(const ConnectionInterfaceEmergencyModeAdaptor::CountryCodeContextPtr &context)
784+{
785+ context->setFinished(mInterface->mPriv->countryCode);
786+}
787+
788 BaseConnectionEmergencyModeInterface::BaseConnectionEmergencyModeInterface()
789 : AbstractConnectionInterface(TP_QT_IFACE_CONNECTION_EMERGENCYMODE),
790 mPriv(new Private(this))
791@@ -86,10 +92,16 @@
792 if (!mPriv->fakeEmergencyNumber.isEmpty()) {
793 finalEmergencyList << mPriv->fakeEmergencyNumber;
794 }
795-
796+
797 Q_EMIT mPriv->adaptee->emergencyNumbersChanged(finalEmergencyList);
798 }
799
800+void BaseConnectionEmergencyModeInterface::setCountryCode(const QString &countryCode)
801+{
802+ mPriv->countryCode = countryCode;
803+ Q_EMIT mPriv->adaptee->countryCodeChanged(mPriv->countryCode);
804+}
805+
806 void BaseConnectionEmergencyModeInterface::setFakeEmergencyNumber(const QString &fakeEmergencyNumber)
807 {
808 mPriv->fakeEmergencyNumber = fakeEmergencyNumber;
809@@ -112,6 +124,7 @@
810 : Tp::AbstractAdaptor(bus, adaptee, parent)
811 {
812 connect(adaptee, SIGNAL(emergencyNumbersChanged(QStringList)), SIGNAL(EmergencyNumbersChanged(QStringList)));
813+ connect(adaptee, SIGNAL(countryCodeChanged(QString)), SIGNAL(CountryCodeChanged(QString)));
814 }
815
816 ConnectionInterfaceEmergencyModeAdaptor::~ConnectionInterfaceEmergencyModeAdaptor()
817@@ -131,3 +144,17 @@
818 Q_ARG(ConnectionInterfaceEmergencyModeAdaptor::EmergencyNumbersContextPtr, ctx));
819 return QStringList();
820 }
821+
822+QString ConnectionInterfaceEmergencyModeAdaptor::CountryCode(const QDBusMessage& dbusMessage)
823+{
824+ if (!adaptee()->metaObject()->indexOfMethod("countryCode(ConnectionInterfaceEmergencyModeAdaptor::CountryCodeContextPtr)") == -1) {
825+ dbusConnection().send(dbusMessage.createErrorReply(TP_QT_ERROR_NOT_IMPLEMENTED, QLatin1String("Not implemented")));
826+ return QString();
827+ }
828+
829+ CountryCodeContextPtr ctx = CountryCodeContextPtr(
830+ new Tp::MethodInvocationContext< QString >(dbusConnection(), dbusMessage));
831+ QMetaObject::invokeMethod(adaptee(), "countryCode",
832+ Q_ARG(ConnectionInterfaceEmergencyModeAdaptor::CountryCodeContextPtr, ctx));
833+ return QString();
834+}
835
836=== modified file 'tests/common/mock/emergencymodeiface.h'
837--- tests/common/mock/emergencymodeiface.h 2015-03-11 17:02:35 +0000
838+++ tests/common/mock/emergencymodeiface.h 2015-08-10 22:00:00 +0000
839@@ -56,6 +56,7 @@
840
841 public Q_SLOTS:
842 void setEmergencyNumbers(const QStringList &numbers);
843+ void setCountryCode(const QString &countryCode);
844
845 protected:
846 BaseConnectionEmergencyModeInterface();
847@@ -83,6 +84,12 @@
848 " <signal name=\"EmergencyNumbersChanged\">\n"
849 " <arg type=\"as\" name=\"numbers\"/>\n"
850 " </signal>\n"
851+" <method name=\"CountryCode\">\n"
852+" <arg direction=\"out\" type=\"s\" name=\"countryCode\"/>\n"
853+" </method>\n"
854+" <signal name=\"CountryCodeChanged\">\n"
855+" <arg type=\"s\" name=\"countryCode\"/>\n"
856+" </signal>\n"
857 " </interface>\n"
858 "")
859
860@@ -91,12 +98,15 @@
861 virtual ~ConnectionInterfaceEmergencyModeAdaptor();
862
863 typedef Tp::MethodInvocationContextPtr< QStringList > EmergencyNumbersContextPtr;
864+ typedef Tp::MethodInvocationContextPtr< QString > CountryCodeContextPtr;
865
866 public Q_SLOTS: // METHODS
867 QStringList EmergencyNumbers(const QDBusMessage& dbusMessage);
868+ QString CountryCode(const QDBusMessage& dbusMessage);
869
870 Q_SIGNALS: // SIGNALS
871 void EmergencyNumbersChanged(const QStringList &numbers);
872+ void CountryCodeChanged(const QString &countryCode);
873 };
874
875
876@@ -110,9 +120,11 @@
877
878 private Q_SLOTS:
879 void emergencyNumbers(const ConnectionInterfaceEmergencyModeAdaptor::EmergencyNumbersContextPtr &context);
880+ void countryCode(const ConnectionInterfaceEmergencyModeAdaptor::CountryCodeContextPtr &context);
881
882 Q_SIGNALS:
883 void emergencyNumbersChanged(const QStringList &numbers);
884+ void countryCodeChanged(const QString &countryCode);
885
886 public:
887 BaseConnectionEmergencyModeInterface *mInterface;
888
889=== modified file 'tests/common/mock/mockconnectiondbus.cpp'
890--- tests/common/mock/mockconnectiondbus.cpp 2015-05-07 20:36:24 +0000
891+++ tests/common/mock/mockconnectiondbus.cpp 2015-08-10 22:00:00 +0000
892@@ -152,6 +152,12 @@
893 mConnection->setEmergencyNumbers(numbers);
894 }
895
896+void MockConnectionDBus::SetCountryCode(const QString &countryCode)
897+{
898+ qDebug() << __PRETTY_FUNCTION__ << countryCode;
899+ mConnection->setCountryCode(countryCode);
900+}
901+
902 QString MockConnectionDBus::Serial()
903 {
904 qDebug() << __PRETTY_FUNCTION__ << mConnection->serial();
905
906=== modified file 'tests/common/mock/mockconnectiondbus.h'
907--- tests/common/mock/mockconnectiondbus.h 2015-04-17 19:52:20 +0000
908+++ tests/common/mock/mockconnectiondbus.h 2015-08-10 22:00:00 +0000
909@@ -47,6 +47,7 @@
910
911 // emergency numbers stuff
912 void SetEmergencyNumbers(const QStringList &numbers);
913+ void SetCountryCode(const QString &countryCode);
914
915 // USSD stuff
916 QString Serial();
917
918=== modified file 'tests/libtelephonyservice/OfonoAccountEntryTest.cpp'
919--- tests/libtelephonyservice/OfonoAccountEntryTest.cpp 2015-05-08 21:57:06 +0000
920+++ tests/libtelephonyservice/OfonoAccountEntryTest.cpp 2015-08-10 22:00:00 +0000
921@@ -36,6 +36,7 @@
922 void testCompareIds_data();
923 void testCompareIds();
924 void testEmergencyNumbers();
925+ void testCountryCode();
926 void testSerial();
927 void testVoicemailIndicator();
928 void testVoicemailNumber();
929@@ -148,6 +149,23 @@
930 QCOMPARE(emergencyNumbers, numbers);
931 }
932
933+void OfonoAccountEntryTest::testCountryCode()
934+{
935+ QSignalSpy countryCodeChangedSpy(mAccount, SIGNAL(countryCodeChanged()));
936+
937+ // check that the countryCode is not empty at startup
938+ QVERIFY(!mAccount->countryCode().isEmpty());
939+ QCOMPARE(mAccount->countryCode(), QString("US"));
940+
941+ QString countryCode("BR");
942+ mMockController->SetCountryCode("BR");
943+ TRY_COMPARE(countryCodeChangedSpy.count(), 1);
944+
945+ QString cc = mAccount->countryCode();
946+
947+ QCOMPARE(cc, countryCode);
948+}
949+
950 void OfonoAccountEntryTest::testSerial()
951 {
952 TRY_COMPARE(mAccount->serial(), mMockController->serial());
953
954=== modified file 'tests/libtelephonyservice/PhoneUtilsTest.cpp'
955--- tests/libtelephonyservice/PhoneUtilsTest.cpp 2015-06-17 19:31:32 +0000
956+++ tests/libtelephonyservice/PhoneUtilsTest.cpp 2015-08-10 22:00:00 +0000
957@@ -21,6 +21,7 @@
958
959 #include "phoneutils.h"
960
961+Q_DECLARE_METATYPE(PhoneUtils::PhoneNumberMatchType)
962
963 class PhoneUtilsTest : public QObject
964 {
965@@ -42,8 +43,6 @@
966 QTest::newRow("number with dash") << "1234-5678" << true;
967 QTest::newRow("number with area code") << "(123)12345678" << true;
968 QTest::newRow("number with extension") << "12345678#123" << true;
969- QTest::newRow("number with comma") << "33333333,1,1" << true;
970- QTest::newRow("number with semicolon") << "33333333;1" << true;
971 QTest::newRow("number with slash") << "+421 2/123 456 78" << true;
972 QTest::newRow("short/emergency number") << "190" << true;
973 QTest::newRow("non phone numbers") << "abcdefg" << false;
974@@ -62,23 +61,21 @@
975 {
976 QTest::addColumn<QString>("number1");
977 QTest::addColumn<QString>("number2");
978- QTest::addColumn<bool>("expectedResult");
979+ QTest::addColumn<PhoneUtils::PhoneNumberMatchType>("expectedResult");
980
981- QTest::newRow("string equal") << "12345678" << "12345678" << true;
982- QTest::newRow("number with dash") << "1234-5678" << "12345678" << true;
983- QTest::newRow("number with area code") << "12312345678" << "12345678" << true;
984- QTest::newRow("number with extension") << "12345678#123" << "12345678" << false;
985- QTest::newRow("both numbers with extension") << "(123)12345678#1" << "12345678#1" << true;
986- QTest::newRow("numbers with different extension") << "1234567#1" << "1234567#2" << false;
987- QTest::newRow("number with comma") << "33333333,1,1" << "33333333" << true;
988- QTest::newRow("both numbers with comma") << "22222222,1" << "22222222,2,1" << true;
989- QTest::newRow("number with semicolon") << "33333333;1" << "33333333" << true;
990- QTest::newRow("both numbers with semicolon") << "22222222;1" << "22222222;2" << true;
991- QTest::newRow("short/emergency numbers") << "190" << "190" << true;
992- QTest::newRow("different numbers") << "12345678" << "1234567" << false;
993- QTest::newRow("both non phone numbers") << "abcdefg" << "abcdefg" << true;
994- QTest::newRow("different non phone numbers") << "abcdefg" << "bcdefg" << false;
995- QTest::newRow("phone number and custom string") << "abc12345678" << "12345678" << false;
996+ QTest::newRow("string equal") << "12345678" << "12345678" << PhoneUtils::NSN_MATCH;
997+ QTest::newRow("number with dash") << "1234-5678" << "12345678" << PhoneUtils::NSN_MATCH;
998+ QTest::newRow("number with area code") << "1231234567" << "1234567" << PhoneUtils::SHORT_NSN_MATCH;
999+ QTest::newRow("number with extension") << "12345678#123" << "12345678" << PhoneUtils::SHORT_NSN_MATCH;
1000+ QTest::newRow("both numbers with extension") << "(123)12345678#1" << "12345678#1" << PhoneUtils::SHORT_NSN_MATCH;
1001+ QTest::newRow("numbers with different extension") << "1234567#1" << "1234567#2" << PhoneUtils::NO_MATCH;
1002+ QTest::newRow("short/emergency numbers") << "190" << "190" << PhoneUtils::EXACT_MATCH;
1003+ QTest::newRow("different short/emergency numbers") << "911" << "11" << PhoneUtils::NO_MATCH;
1004+ QTest::newRow("different numbers") << "12345678" << "1234567" << PhoneUtils::NO_MATCH;
1005+ QTest::newRow("both non phone numbers") << "abcdefg" << "abcdefg" << PhoneUtils::EXACT_MATCH;
1006+ QTest::newRow("different non phone numbers") << "abcdefg" << "bcdefg" << PhoneUtils::INVALID_NUMBER;
1007+ QTest::newRow("phone number and custom string") << "abc12345678" << "12345678" << PhoneUtils::NSN_MATCH;
1008+ QTest::newRow("phone number with slash") << "+421 2/123 456 78" << "212345678" << PhoneUtils::NSN_MATCH;
1009 // FIXME: check what other cases we need to test here"
1010 }
1011
1012@@ -86,9 +83,9 @@
1013 {
1014 QFETCH(QString, number1);
1015 QFETCH(QString, number2);
1016- QFETCH(bool, expectedResult);
1017+ QFETCH(PhoneUtils::PhoneNumberMatchType, expectedResult);
1018
1019- bool result = PhoneUtils::comparePhoneNumbers(number1, number2);
1020+ PhoneUtils::PhoneNumberMatchType result = PhoneUtils::comparePhoneNumbers(number1, number2);
1021 QCOMPARE(result, expectedResult);
1022 }
1023

Subscribers

People subscribed via source and target branches