cordova-plugin-appice 2.0.9 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/example/config.xml +59 -59
- package/example/package.json +38 -38
- package/example/res/ai_android.pem +40 -40
- package/example/www/css/index.css +116 -116
- package/example/www/index.html +62 -62
- package/example/www/js/index.js +111 -102
- package/package.json +20 -2
- package/plugin.xml +193 -128
- package/scripts/BeforeAndroidBuilt.js +165 -102
- package/scripts/BeforeIosBuilt.js +35 -35
- package/scripts/androidAfterPluginAdd.js +67 -67
- package/scripts/androidAfterPluginRm.js +112 -112
- package/scripts/iOSAfterPluginAdd.js +58 -58
- package/scripts/iOSAfterPluginRm.js +30 -31
- package/src/android/AppICEFCMPush.java +26 -0
- package/src/android/AppICEMFPPush.java +67 -0
- package/{libcordova/src/main/java/com/appice/cordova → src/android}/AppICEPlugin.java +984 -968
- package/src/android/AppICEPushHandler.java +163 -0
- package/{libcordova/src/main/java/com/appice/cordova → src/android}/CampaignCampsReceiver.java +94 -95
- package/{libcordova/src/main/java/com/appice/cordova → src/android}/NotificationEventService.java +64 -55
- package/src/build.gradle +4 -2
- package/src/ios/AppDelegate+AppICEPlugin.m +178 -89
- package/src/ios/AppICEPlugin.h +3 -0
- package/src/ios/AppICEPlugin.m +55 -10
- package/www/AppICE.js +261 -228
- package/libcordova/android-release-aar.gradle +0 -63
- package/libcordova/build.gradle +0 -50
- package/libcordova/cordova.jar +0 -0
- package/libcordova/proguard-rules.pro +0 -21
- package/libcordova/src/main/AndroidManifest.xml +0 -24
- package/libcordova/src/main/res/values/strings.xml +0 -3
- package/src/firebase/modified/modified.iml +0 -11
- package/src/firebase/original/original.iml +0 -11
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
package com.appice.cordova;
|
|
2
|
+
|
|
3
|
+
import android.app.PendingIntent;
|
|
4
|
+
import android.content.Context;
|
|
5
|
+
import android.content.Intent;
|
|
6
|
+
import android.content.pm.PackageManager;
|
|
7
|
+
import android.content.pm.ResolveInfo;
|
|
8
|
+
import android.os.Build;
|
|
9
|
+
import android.os.Bundle;
|
|
10
|
+
import android.util.Log;
|
|
11
|
+
|
|
12
|
+
import androidx.annotation.NonNull;
|
|
13
|
+
|
|
14
|
+
import com.google.firebase.messaging.RemoteMessage;
|
|
15
|
+
import com.ibm.mobilefirstplatform.clientsdk.android.push.api.MFPPushIntentService;
|
|
16
|
+
import com.ibm.mobilefirstplatform.clientsdk.android.push.internal.MFPInternalPushMessage;
|
|
17
|
+
|
|
18
|
+
import org.json.JSONObject;
|
|
19
|
+
|
|
20
|
+
import java.util.List;
|
|
21
|
+
import java.util.Map;
|
|
22
|
+
|
|
23
|
+
import semusi.activitysdk.ContextSdk;
|
|
24
|
+
import semusi.context.utility.Utility;
|
|
25
|
+
|
|
26
|
+
/* import path for MainActivity */
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/*=============================*/
|
|
30
|
+
|
|
31
|
+
public class AppICEPushHandler {
|
|
32
|
+
private static final String TAG = AppICEPushHandler.class.getSimpleName();
|
|
33
|
+
private static final String ACTION = "com.appice.campaignEvent";
|
|
34
|
+
private static final String PUSH_PAYLOAD = "ai_content";
|
|
35
|
+
private static final String MESSAGE = "message";
|
|
36
|
+
private static final String EXTERNAL_TYPE = "et";
|
|
37
|
+
private static final String DEEPLINK = "dl";
|
|
38
|
+
private static final String LANDING_PAGE = "lp";
|
|
39
|
+
|
|
40
|
+
public void onMessageReceived(RemoteMessage message, Context context) {
|
|
41
|
+
Map<String, String> data = message.getData();
|
|
42
|
+
|
|
43
|
+
if (isAppICEPush(message) && !checkIfPayloadHaveExternalUrl(message)) {
|
|
44
|
+
ContextSdk.handleAppICEPush(message, createPendingIntent(data.get(MESSAGE), context), deletePendingIntent(context), context);
|
|
45
|
+
} else if (isAppICEPush(message) && checkIfPayloadHaveExternalUrl(message)) {
|
|
46
|
+
ContextSdk.handleAppICEPush(message, context);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public void onNewToken(@NonNull String s, Context context) {
|
|
51
|
+
ContextSdk.checkIfDeviceRegisterToFCM(context);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public boolean isAppICEPush(RemoteMessage message) {
|
|
55
|
+
return ContextSdk.isAppICENotification(message);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public static PendingIntent createPendingIntent(String message, Context mContext) {
|
|
59
|
+
PendingIntent pendingIntent = null;
|
|
60
|
+
pendingIntent = internalCreatePendingIntent(message, mContext);
|
|
61
|
+
return pendingIntent;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public static PendingIntent deletePendingIntent(Context mContext) {
|
|
65
|
+
PendingIntent pendingIntent = null;
|
|
66
|
+
pendingIntent = internalDeletePendingIntent(mContext);
|
|
67
|
+
return pendingIntent;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//================================
|
|
71
|
+
// internal helper function
|
|
72
|
+
//================================
|
|
73
|
+
private static PendingIntent internalCreatePendingIntent(String message, Context mContext) {
|
|
74
|
+
try {
|
|
75
|
+
Class<?> launcherActivityClass = getLauncherActivityClass(mContext);
|
|
76
|
+
if (launcherActivityClass == null) {
|
|
77
|
+
// please use your launch activity here for example
|
|
78
|
+
// launcherActivityClass = MainActivity.class;
|
|
79
|
+
}
|
|
80
|
+
Intent clickIntent = new Intent(mContext, launcherActivityClass);
|
|
81
|
+
Bundle bundle = new Bundle();
|
|
82
|
+
final Intent newIntent = new Intent();
|
|
83
|
+
newIntent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
|
|
84
|
+
newIntent.setAction(ACTION);
|
|
85
|
+
clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
|
86
|
+
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
|
87
|
+
newIntent.putExtra(PUSH_PAYLOAD, message);
|
|
88
|
+
bundle.putString(PUSH_PAYLOAD, message);
|
|
89
|
+
clickIntent.putExtras(bundle);
|
|
90
|
+
|
|
91
|
+
Utility.sendEventToListener(mContext, newIntent);
|
|
92
|
+
return PendingIntent.getActivity(
|
|
93
|
+
mContext, 1, clickIntent, getPendingIntentFlags());
|
|
94
|
+
} catch (Throwable e) {
|
|
95
|
+
Log.e(TAG, "createPendingIntent: error ", e);
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public static PendingIntent internalDeletePendingIntent(Context mContext) {
|
|
101
|
+
try {
|
|
102
|
+
// please use your launch activity here
|
|
103
|
+
Intent clickIntent = new Intent(mContext, CampaignCampsReceiver.class);
|
|
104
|
+
return PendingIntent.getBroadcast(mContext, 1, clickIntent,
|
|
105
|
+
getPendingIntentFlags());
|
|
106
|
+
} catch (Throwable t) {
|
|
107
|
+
Log.e(TAG, "deletePendingIntent: error ", t);
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private static int getPendingIntentFlags() {
|
|
113
|
+
int flag;
|
|
114
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
115
|
+
flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE;
|
|
116
|
+
} else {
|
|
117
|
+
flag = PendingIntent.FLAG_UPDATE_CURRENT;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return flag;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private boolean checkIfPayloadHaveExternalUrl(RemoteMessage message) {
|
|
124
|
+
Map<String, String> map = message.getData();
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
if (map != null) {
|
|
128
|
+
String messageStr = map.get(MESSAGE);
|
|
129
|
+
if (messageStr != null && messageStr.length() > 0) {
|
|
130
|
+
JSONObject messageObj = new JSONObject(messageStr);
|
|
131
|
+
String type = messageObj.optString(EXTERNAL_TYPE);
|
|
132
|
+
if (type.equalsIgnoreCase(DEEPLINK)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (type.equalsIgnoreCase(LANDING_PAGE)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch (Throwable t) {
|
|
141
|
+
Log.e(TAG, "checkIfPayloadHaveExternalUrl: error ", t);
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private static Class<?> getLauncherActivityClass(Context context) {
|
|
147
|
+
PackageManager packageManager = context.getPackageManager();
|
|
148
|
+
Intent intent = new Intent(Intent.ACTION_MAIN);
|
|
149
|
+
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
|
150
|
+
ResolveInfo resolveInfo = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
|
|
151
|
+
|
|
152
|
+
if (resolveInfo != null && resolveInfo.activityInfo != null) {
|
|
153
|
+
String className = resolveInfo.activityInfo.name;
|
|
154
|
+
try {
|
|
155
|
+
return Class.forName(className);
|
|
156
|
+
} catch (Throwable e) {
|
|
157
|
+
Log.e(TAG, "getLauncherActivityClass: error ", e);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
package/{libcordova/src/main/java/com/appice/cordova → src/android}/CampaignCampsReceiver.java
RENAMED
|
@@ -1,95 +1,94 @@
|
|
|
1
|
-
package com.appice.cordova;
|
|
2
|
-
|
|
3
|
-
import android.content.BroadcastReceiver;
|
|
4
|
-
import android.content.Context;
|
|
5
|
-
import android.content.Intent;
|
|
6
|
-
import android.net.Uri;
|
|
7
|
-
import android.os.Bundle;
|
|
8
|
-
|
|
9
|
-
import org.json.
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
import java.
|
|
13
|
-
import java.util.
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
1
|
+
package com.appice.cordova;
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver;
|
|
4
|
+
import android.content.Context;
|
|
5
|
+
import android.content.Intent;
|
|
6
|
+
import android.net.Uri;
|
|
7
|
+
import android.os.Bundle;
|
|
8
|
+
import android.util.Log;
|
|
9
|
+
import org.json.JSONObject;
|
|
10
|
+
import java.net.URLDecoder;
|
|
11
|
+
import java.util.HashMap;
|
|
12
|
+
import java.util.Iterator;
|
|
13
|
+
import java.util.Set;
|
|
14
|
+
import semusi.activitysdk.ContextSdk;
|
|
15
|
+
|
|
16
|
+
public class CampaignCampsReceiver extends BroadcastReceiver {
|
|
17
|
+
private static final String TAG = CampaignCampsReceiver.class.getSimpleName();
|
|
18
|
+
|
|
19
|
+
@Override
|
|
20
|
+
public void onReceive(Context context, Intent intent) {
|
|
21
|
+
try {
|
|
22
|
+
sendCallback(intent.getExtras(), context);
|
|
23
|
+
} catch (Throwable e) {
|
|
24
|
+
Log.e(TAG, "onReceive: error ", e);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public void sendCallback(Bundle extra, Context context) {
|
|
29
|
+
try {
|
|
30
|
+
// value of user click on campaign
|
|
31
|
+
String tap = extra.getString("tap");
|
|
32
|
+
// value of clicked is True/False based upon user click or not
|
|
33
|
+
String url = extra.getString("url");
|
|
34
|
+
String cdata = extra.getString("cdata");
|
|
35
|
+
String pushCallback = extra.getString("ai_content");
|
|
36
|
+
|
|
37
|
+
JSONObject json = new JSONObject();
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// gather deeplink data
|
|
41
|
+
if (url != null && url.length() > 0) {
|
|
42
|
+
HashMap<String, Object> deeplinkData = ContextSdk.gatherDeepLinkData(Uri.parse(url));
|
|
43
|
+
Set<String> keys = deeplinkData.keySet();
|
|
44
|
+
for (String key : keys) {
|
|
45
|
+
try {
|
|
46
|
+
json.put(key, deeplinkData.get(key));
|
|
47
|
+
} catch (Throwable e) {
|
|
48
|
+
Log.e(TAG, "sendCallback: error ",e );
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch (Throwable e) {
|
|
53
|
+
Log.e(TAG, "sendCallback: error ",e );
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Gather extra data from json object root
|
|
57
|
+
try {
|
|
58
|
+
if (cdata != null && cdata.length() > 0) {
|
|
59
|
+
JSONObject croot = new JSONObject(cdata);
|
|
60
|
+
if (croot != null && croot.length() > 0) {
|
|
61
|
+
Iterator<String> it = croot.keys();
|
|
62
|
+
while (it.hasNext()) {
|
|
63
|
+
String key = it.next();
|
|
64
|
+
Object obj = croot.get(key);
|
|
65
|
+
if (obj.getClass().equals(String.class))
|
|
66
|
+
json.put(key, URLDecoder.decode((String) obj, "UTF-8"));
|
|
67
|
+
else
|
|
68
|
+
json.put(key, obj);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (Throwable e) {
|
|
73
|
+
Log.e(TAG, "sendCallback: error ",e );
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
json.put("tap", tap);
|
|
77
|
+
json.put("ai_content", pushCallback);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
if (url != null && url.length() > 0) {
|
|
81
|
+
Uri path = Uri.parse(url);
|
|
82
|
+
json.put("host", path.getHost());
|
|
83
|
+
json.put("path", path.getPath());
|
|
84
|
+
}
|
|
85
|
+
} catch (Throwable e) {
|
|
86
|
+
Log.e(TAG, "sendCallback: error ",e );
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
AppICEPlugin.sendNotification(json, context);
|
|
90
|
+
} catch (Throwable e) {
|
|
91
|
+
Log.e(TAG, "sendCallback: error ",e );
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
package/{libcordova/src/main/java/com/appice/cordova → src/android}/NotificationEventService.java
RENAMED
|
@@ -1,55 +1,64 @@
|
|
|
1
|
-
package com.appice.cordova;
|
|
2
|
-
|
|
3
|
-
import android.app.job.JobParameters;
|
|
4
|
-
import android.app.job.JobService;
|
|
5
|
-
import android.os.Build;
|
|
6
|
-
import android.os.Bundle;
|
|
7
|
-
import android.os.PersistableBundle;
|
|
8
|
-
|
|
9
|
-
import
|
|
10
|
-
import java.util.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
1
|
+
package com.appice.cordova;
|
|
2
|
+
|
|
3
|
+
import android.app.job.JobParameters;
|
|
4
|
+
import android.app.job.JobService;
|
|
5
|
+
import android.os.Build;
|
|
6
|
+
import android.os.Bundle;
|
|
7
|
+
import android.os.PersistableBundle;
|
|
8
|
+
import android.util.Log;
|
|
9
|
+
import androidx.work.Configuration;
|
|
10
|
+
import java.util.Timer;
|
|
11
|
+
import java.util.TimerTask;
|
|
12
|
+
import java.util.concurrent.atomic.AtomicReference;
|
|
13
|
+
|
|
14
|
+
public class NotificationEventService extends JobService {
|
|
15
|
+
private static final String TAG = NotificationEventService.class.getSimpleName();
|
|
16
|
+
|
|
17
|
+
public NotificationEventService() {
|
|
18
|
+
AtomicReference<Configuration.Builder> notificationBuilder = new AtomicReference<>(new Configuration.Builder());
|
|
19
|
+
notificationBuilder.get().setJobSchedulerJobIdRange(0, 1000);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Override
|
|
23
|
+
public boolean onStartJob(JobParameters params) {
|
|
24
|
+
if (params != null) {
|
|
25
|
+
PersistableBundle bundle = null;
|
|
26
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
|
27
|
+
bundle = params.getExtras();
|
|
28
|
+
}
|
|
29
|
+
if (bundle != null) {
|
|
30
|
+
final Bundle bundle1 = toBundle(bundle);
|
|
31
|
+
if (bundle1 != null) {
|
|
32
|
+
new Timer("AppICE-NotifService-Timer").schedule(new TimerTask() {
|
|
33
|
+
@Override
|
|
34
|
+
public void run() {
|
|
35
|
+
CampaignCampsReceiver rc = new CampaignCampsReceiver();
|
|
36
|
+
rc.sendCallback(bundle1, getApplicationContext());
|
|
37
|
+
}
|
|
38
|
+
}, 1000);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public static Bundle toBundle(PersistableBundle persistableBundle) {
|
|
46
|
+
try {
|
|
47
|
+
if (persistableBundle == null) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
Bundle bundle = new Bundle();
|
|
51
|
+
if (Build.VERSION.SDK_INT >= 21)
|
|
52
|
+
bundle.putAll(persistableBundle);
|
|
53
|
+
return bundle;
|
|
54
|
+
} catch (Throwable e) {
|
|
55
|
+
Log.e(TAG, "toBundle: error ", e);
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Override
|
|
61
|
+
public boolean onStopJob(JobParameters params) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/build.gradle
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
allprojects {
|
|
2
2
|
apply from: 'repositories.gradle'
|
|
3
3
|
repositories {
|
|
4
|
-
|
|
4
|
+
maven { url "https://jitpack.io" }
|
|
5
|
+
maven {
|
|
5
6
|
url "https://gitlab.com/api/v4/projects/10636887/packages/maven"
|
|
6
7
|
credentials(HttpHeaderCredentials) {
|
|
7
8
|
name = "Deploy-Token"
|
|
8
|
-
value = ""
|
|
9
|
+
value = "PJMsxXdArqsmqDx4x5B6"
|
|
9
10
|
}
|
|
10
11
|
authentication {
|
|
11
12
|
header(HttpHeaderAuthentication)
|
|
12
13
|
}
|
|
13
14
|
}
|
|
15
|
+
|
|
14
16
|
}
|
|
15
17
|
}
|