1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.telephony.metrics;
18 
19 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_ATTEMPTED_BITMASK;
20 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_SUCCEEDED_BITMASK;
21 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_VALIDATION_RESULT;
22 import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS;
23 import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE;
24 import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS;
25 
26 import android.annotation.ElapsedRealtimeLong;
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.net.ConnectivityDiagnosticsManager;
30 import android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback;
31 import android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
32 import android.net.ConnectivityDiagnosticsManager.DataStallReport;
33 import android.net.NetworkCapabilities;
34 import android.net.NetworkRequest;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.os.HandlerThread;
38 import android.os.PersistableBundle;
39 import android.os.SystemClock;
40 import android.telephony.AccessNetworkConstants;
41 import android.telephony.Annotation.NetworkType;
42 import android.telephony.CellSignalStrength;
43 import android.telephony.NetworkRegistrationInfo;
44 import android.telephony.ServiceState;
45 import android.telephony.TelephonyManager;
46 import android.telephony.data.DataCallResponse;
47 import android.telephony.data.DataCallResponse.LinkStatus;
48 import android.text.TextUtils;
49 
50 import com.android.internal.telephony.Phone;
51 import com.android.internal.telephony.PhoneFactory;
52 import com.android.internal.telephony.TelephonyStatsLog;
53 import com.android.internal.telephony.data.DataNetwork;
54 import com.android.internal.telephony.data.DataNetworkController;
55 import com.android.internal.telephony.data.DataNetworkController.DataNetworkControllerCallback;
56 import com.android.internal.telephony.data.DataStallRecoveryManager;
57 import com.android.internal.telephony.flags.FeatureFlags;
58 import com.android.internal.telephony.subscription.SubscriptionInfoInternal;
59 import com.android.internal.telephony.subscription.SubscriptionManagerService;
60 import com.android.telephony.Rlog;
61 
62 import java.util.Set;
63 import java.util.concurrent.Executor;
64 import java.util.concurrent.TimeUnit;
65 
66 /**
67  * Generates metrics related to data stall recovery events per phone ID for the pushed atom.
68  */
69 public class DataStallRecoveryStats {
70 
71     /**
72      * Value indicating that link bandwidth is unspecified.
73      * Copied from {@code NetworkCapabilities#LINK_BANDWIDTH_UNSPECIFIED}
74      */
75     private static final int LINK_BANDWIDTH_UNSPECIFIED = 0;
76 
77     private static final String TAG = "DSRS-";
78 
79     private static final int UNSET_DIAGNOSTIC_STATE = -1;
80 
81     private static final long REFRESH_DURATION_IN_MILLIS = TimeUnit.MINUTES.toMillis(3);
82 
83     // Handler to upload metrics.
84     private final @NonNull Handler mHandler;
85 
86     private final @NonNull String mTag;
87     private final @NonNull Phone mPhone;
88     private final @NonNull TelephonyManager mTelephonyManager;
89     private final @NonNull FeatureFlags mFeatureFlags;
90 
91     // Flag to control the DSRS diagnostics
92     private final boolean mIsDsrsDiagnosticsEnabled;
93 
94     // The interface name of the internet network.
95     private @Nullable String mIfaceName = null;
96 
97     /* Metrics and stats data variables */
98     // Record metrics refresh time in milliseconds to decide whether to refresh data again
99     @ElapsedRealtimeLong
100     private long mMetricsReflashTime = 0L;
101     private int mPhoneId = 0;
102     private int mCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
103     private int mConvertedMccMnc = -1;
104     private int mSignalStrength = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
105     private int mBand = 0;
106     // The RAT used for data (including IWLAN).
107     private @NetworkType int mRat = TelephonyManager.NETWORK_TYPE_UNKNOWN;
108     private boolean mIsOpportunistic = false;
109     private boolean mIsMultiSim = false;
110     private int mNetworkRegState = NetworkRegistrationInfo
111                     .REGISTRATION_STATE_NOT_REGISTERED_OR_SEARCHING;
112     // Info of the other device in case of DSDS
113     private int mOtherSignalStrength = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
114     private int mOtherNetworkRegState = NetworkRegistrationInfo
115                     .REGISTRATION_STATE_NOT_REGISTERED_OR_SEARCHING;
116     // Link status of the data network
117     private @LinkStatus int mInternetLinkStatus = DataCallResponse.LINK_STATUS_UNKNOWN;
118 
119     // The link bandwidth of the data network
120     private int mLinkDownBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED;
121     private int mLinkUpBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED;
122 
123     // Connectivity diagnostics states
124     private int mNetworkProbesResult = UNSET_DIAGNOSTIC_STATE;
125     private int mNetworkProbesType = UNSET_DIAGNOSTIC_STATE;
126     private int mNetworkValidationResult = UNSET_DIAGNOSTIC_STATE;
127     private int mTcpMetricsCollectionPeriodMillis = UNSET_DIAGNOSTIC_STATE;
128     private int mTcpPacketFailRate = UNSET_DIAGNOSTIC_STATE;
129     private int mDnsConsecutiveTimeouts = UNSET_DIAGNOSTIC_STATE;
130 
131     private ConnectivityDiagnosticsManager mConnectivityDiagnosticsManager = null;
132     private ConnectivityDiagnosticsCallback mConnectivityDiagnosticsCallback = null;
133     private static final Executor INLINE_EXECUTOR = x -> x.run();
134 
135     /**
136      * Constructs a new instance of {@link DataStallRecoveryStats}.
137      */
DataStallRecoveryStats( @onNull final Phone phone, @NonNull FeatureFlags featureFlags, @NonNull final DataNetworkController dataNetworkController)138     public DataStallRecoveryStats(
139             @NonNull final Phone phone,
140             @NonNull FeatureFlags featureFlags,
141             @NonNull final DataNetworkController dataNetworkController) {
142         mTag = TAG + phone.getPhoneId();
143         mPhone = phone;
144         mFeatureFlags = featureFlags;
145 
146         HandlerThread handlerThread = new HandlerThread(mTag + "-thread");
147         handlerThread.start();
148         mHandler = new Handler(handlerThread.getLooper());
149         mTelephonyManager = mPhone.getContext().getSystemService(TelephonyManager.class);
150 
151         dataNetworkController.registerDataNetworkControllerCallback(
152                 new DataNetworkControllerCallback(mHandler::post) {
153                 @Override
154                 public void onConnectedInternetDataNetworksChanged(
155                         @NonNull Set<DataNetwork> internetNetworks) {
156                     mIfaceName = null;
157                     for (DataNetwork dataNetwork : internetNetworks) {
158                         mIfaceName = dataNetwork.getLinkProperties().getInterfaceName();
159                         break;
160                     }
161                 }
162 
163                 @Override
164                 public void onPhysicalLinkStatusChanged(@LinkStatus int status) {
165                     mInternetLinkStatus = status;
166                 }
167             });
168 
169         mIsDsrsDiagnosticsEnabled = mFeatureFlags.dsrsDiagnosticsEnabled();
170         if (mIsDsrsDiagnosticsEnabled) {
171             try {
172                 // Register ConnectivityDiagnosticsCallback to get diagnostics states
173                 mConnectivityDiagnosticsManager =
174                     mPhone.getContext().getSystemService(ConnectivityDiagnosticsManager.class);
175                 mConnectivityDiagnosticsCallback = new ConnectivityDiagnosticsCallback() {
176                     @Override
177                     public void onConnectivityReportAvailable(@NonNull ConnectivityReport report) {
178                         PersistableBundle bundle = report.getAdditionalInfo();
179                         mNetworkProbesResult = bundle.getInt(KEY_NETWORK_PROBES_SUCCEEDED_BITMASK);
180                         mNetworkProbesType = bundle.getInt(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK);
181                         mNetworkValidationResult = bundle.getInt(KEY_NETWORK_VALIDATION_RESULT);
182                     }
183 
184                     @Override
185                     public void onDataStallSuspected(@NonNull DataStallReport report) {
186                         PersistableBundle bundle = report.getStallDetails();
187                         mTcpMetricsCollectionPeriodMillis =
188                             bundle.getInt(KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS);
189                         mTcpPacketFailRate = bundle.getInt(KEY_TCP_PACKET_FAIL_RATE);
190                         mDnsConsecutiveTimeouts = bundle.getInt(KEY_DNS_CONSECUTIVE_TIMEOUTS);
191                     }
192                 };
193                 mConnectivityDiagnosticsManager.registerConnectivityDiagnosticsCallback(
194                     new NetworkRequest.Builder()
195                         .clearCapabilities()
196                         .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
197                         .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
198                         .build(),
199                         INLINE_EXECUTOR,
200                         mConnectivityDiagnosticsCallback
201                 );
202             } catch (Exception e) {
203                 mConnectivityDiagnosticsManager = null;
204                 mConnectivityDiagnosticsCallback = null;
205             }
206         }
207     }
208 
209     /**
210      * Create and push new atom when there is a data stall recovery event.
211      *
212      * @param action The recovery action.
213      * @param isRecovered Whether the data stall has been recovered.
214      * @param duration The duration from data stall occurred in milliseconds.
215      * @param reason The reason for the recovery.
216      * @param isFirstValidation Whether this is the first validation after recovery.
217      * @param durationOfAction The duration of the current action in milliseconds.
218      */
uploadMetrics( @ataStallRecoveryManager.RecoveryAction int action, boolean isRecovered, int duration, @DataStallRecoveryManager.RecoveredReason int reason, boolean isFirstValidation, int durationOfAction)219     public void uploadMetrics(
220             @DataStallRecoveryManager.RecoveryAction int action,
221             boolean isRecovered,
222             int duration,
223             @DataStallRecoveryManager.RecoveredReason int reason,
224             boolean isFirstValidation,
225             int durationOfAction) {
226 
227         mHandler.post(() -> {
228             // Update data stall stats
229             log("Set recovery action to " + action);
230 
231             // Refreshes the metrics data.
232             try {
233                 refreshMetricsData();
234             } catch (Exception e) {
235                 loge("The metrics data cannot be refreshed.", e);
236                 return;
237             }
238 
239             TelephonyStatsLog.write(
240                     TelephonyStatsLog.DATA_STALL_RECOVERY_REPORTED,
241                     mCarrierId,
242                     mRat,
243                     mSignalStrength,
244                     action,
245                     mIsOpportunistic,
246                     mIsMultiSim,
247                     mBand,
248                     isRecovered,
249                     duration,
250                     reason,
251                     mOtherSignalStrength,
252                     mOtherNetworkRegState,
253                     mNetworkRegState,
254                     isFirstValidation,
255                     mPhoneId,
256                     durationOfAction,
257                     mInternetLinkStatus,
258                     mLinkUpBandwidthKbps,
259                     mLinkDownBandwidthKbps);
260 
261             log("Upload stats: "
262                     + "Action:"
263                     + action
264                     + ", Recovered:"
265                     + isRecovered
266                     + ", Duration:"
267                     + duration
268                     + ", Reason:"
269                     + reason
270                     + ", First validation:"
271                     + isFirstValidation
272                     + ", Duration of action:"
273                     + durationOfAction
274                     + ", "
275                     + this);
276         });
277     }
278 
279     /**
280      * Refreshes the metrics data.
281      */
refreshMetricsData()282     private void refreshMetricsData() {
283         logd("Refreshes the metrics data.");
284         // Update the metrics reflash time
285         mMetricsReflashTime = SystemClock.elapsedRealtime();
286         // Update phone id/carrier id and signal strength
287         mPhoneId = mPhone.getPhoneId() + 1;
288         mCarrierId = mPhone.getCarrierId();
289         mSignalStrength = mPhone.getSignalStrength().getLevel();
290         if (mIsDsrsDiagnosticsEnabled) {
291             // Get the MCCMNC and convert it to an int
292             String networkOperator = mTelephonyManager.getNetworkOperator();
293             if (!TextUtils.isEmpty(networkOperator)) {
294                 try {
295                     mConvertedMccMnc = Integer.parseInt(networkOperator);
296                 } catch (NumberFormatException e) {
297                     loge("Invalid MCCMNC format: " + networkOperator);
298                     mConvertedMccMnc = -1;
299                 }
300             } else {
301                 mConvertedMccMnc = -1;
302             }
303         }
304 
305         // Update the bandwidth.
306         updateBandwidths();
307 
308         // Update the RAT and band.
309         updateRatAndBand();
310 
311         // Update the opportunistic state.
312         mIsOpportunistic = getIsOpportunistic(mPhone);
313 
314         // Update the multi-SIM state.
315         mIsMultiSim = SimSlotState.getCurrentState().numActiveSims > 1;
316 
317         // Update the network registration state.
318         updateNetworkRegState();
319 
320         // Update the DSDS information.
321         updateDsdsInfo();
322     }
323 
324     /**
325      * Updates the bandwidth for the current data network.
326      */
updateBandwidths()327     private void updateBandwidths() {
328         mLinkDownBandwidthKbps = mLinkUpBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED;
329 
330         if (mIfaceName == null) {
331             loge("Interface name is null");
332             return;
333         }
334 
335         DataNetworkController dataNetworkController = mPhone.getDataNetworkController();
336         if (dataNetworkController == null) {
337             loge("DataNetworkController is null");
338             return;
339         }
340 
341         DataNetwork dataNetwork = dataNetworkController.getDataNetworkByInterface(mIfaceName);
342         if (dataNetwork == null) {
343             loge("DataNetwork is null");
344             return;
345         }
346         NetworkCapabilities networkCapabilities = dataNetwork.getNetworkCapabilities();
347         if (networkCapabilities == null) {
348             loge("NetworkCapabilities is null");
349             return;
350         }
351 
352         mLinkDownBandwidthKbps = networkCapabilities.getLinkDownstreamBandwidthKbps();
353         mLinkUpBandwidthKbps = networkCapabilities.getLinkUpstreamBandwidthKbps();
354     }
355 
updateRatAndBand()356     private void updateRatAndBand() {
357         mRat = TelephonyManager.NETWORK_TYPE_UNKNOWN;
358         mBand = 0;
359         ServiceState serviceState = mPhone.getServiceState();
360         if (serviceState == null) {
361             loge("ServiceState is null");
362             return;
363         }
364 
365         mRat = serviceState.getDataNetworkType();
366         mBand =
367             (mRat == TelephonyManager.NETWORK_TYPE_IWLAN) ? 0 : ServiceStateStats.getBand(mPhone);
368     }
369 
getIsOpportunistic(@onNull Phone phone)370     private static boolean getIsOpportunistic(@NonNull Phone phone) {
371         SubscriptionInfoInternal subInfo = SubscriptionManagerService.getInstance()
372                 .getSubscriptionInfoInternal(phone.getSubId());
373         return subInfo != null && subInfo.isOpportunistic();
374     }
375 
updateNetworkRegState()376     private void updateNetworkRegState() {
377         mNetworkRegState = NetworkRegistrationInfo
378             .REGISTRATION_STATE_NOT_REGISTERED_OR_SEARCHING;
379 
380         NetworkRegistrationInfo phoneRegInfo = mPhone.getServiceState()
381                 .getNetworkRegistrationInfo(NetworkRegistrationInfo.DOMAIN_PS,
382                 AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
383         if (phoneRegInfo != null) {
384             mNetworkRegState = phoneRegInfo.getRegistrationState();
385         }
386     }
387 
updateDsdsInfo()388     private void updateDsdsInfo() {
389         mOtherSignalStrength = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
390         mOtherNetworkRegState = NetworkRegistrationInfo
391             .REGISTRATION_STATE_NOT_REGISTERED_OR_SEARCHING;
392         for (Phone otherPhone : PhoneFactory.getPhones()) {
393             if (otherPhone.getPhoneId() == mPhone.getPhoneId()) continue;
394             if (!getIsOpportunistic(otherPhone)) {
395                 mOtherSignalStrength = otherPhone.getSignalStrength().getLevel();
396                 NetworkRegistrationInfo regInfo = otherPhone.getServiceState()
397                         .getNetworkRegistrationInfo(NetworkRegistrationInfo.DOMAIN_PS,
398                         AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
399                 if (regInfo != null) {
400                     mOtherNetworkRegState = regInfo.getRegistrationState();
401                 }
402                 break;
403             }
404         }
405     }
406 
407     /**
408      * Return bundled data stall recovery metrics data.
409      *
410      * @param action The recovery action.
411      * @param isRecovered Whether the data stall has been recovered.
412      * @param duration The duration from data stall occurred in milliseconds.
413      * @param reason The reason for the recovery.
414      * @param validationCount The total number of validation duration a data stall.
415      * @param actionValidationCount The number of validation for current action during a data stall
416      * @param durationOfAction The duration of the current action in milliseconds.
417      */
getDataStallRecoveryMetricsData( @ataStallRecoveryManager.RecoveryAction int action, boolean isRecovered, int duration, @DataStallRecoveryManager.RecoveredReason int reason, int validationCount, int actionValidationCount, int durationOfAction)418     public Bundle getDataStallRecoveryMetricsData(
419             @DataStallRecoveryManager.RecoveryAction int action,
420             boolean isRecovered,
421             int duration,
422             @DataStallRecoveryManager.RecoveredReason int reason,
423             int validationCount,
424             int actionValidationCount,
425             int durationOfAction) {
426 
427         if (mIsDsrsDiagnosticsEnabled) {
428             // Refresh data if the data has not been updated within 3 minutes
429             final long refreshDuration = SystemClock.elapsedRealtime() - mMetricsReflashTime;
430             if (refreshDuration > REFRESH_DURATION_IN_MILLIS) {
431                 // Refreshes the metrics data.
432                 try {
433                     refreshMetricsData();
434                 } catch (Exception e) {
435                     loge("The metrics data cannot be refreshed.", e);
436                 }
437             }
438         }
439 
440         Bundle bundle = new Bundle();
441 
442         if (mIsDsrsDiagnosticsEnabled) {
443             bundle.putInt("Action", action);
444             bundle.putInt("IsRecovered", isRecovered ? 1 : 0);
445             bundle.putInt("Duration", duration);
446             bundle.putInt("Reason", reason);
447             bundle.putInt("DurationOfAction", durationOfAction);
448             bundle.putInt("ValidationCount", validationCount);
449             bundle.putInt("ActionValidationCount", actionValidationCount);
450             bundle.putInt("PhoneId", mPhoneId);
451             bundle.putInt("CarrierId", mCarrierId);
452             bundle.putInt("MccMnc", mConvertedMccMnc);
453             bundle.putInt("SignalStrength", mSignalStrength);
454             bundle.putInt("Band", mBand);
455             bundle.putInt("Rat", mRat);
456             bundle.putInt("IsOpportunistic", mIsOpportunistic ? 1 : 0);
457             bundle.putInt("IsMultiSim", mIsMultiSim ? 1 : 0);
458             bundle.putInt("NetworkRegState", mNetworkRegState);
459             bundle.putInt("OtherSignalStrength", mOtherSignalStrength);
460             bundle.putInt("OtherNetworkRegState", mOtherNetworkRegState);
461             bundle.putInt("InternetLinkStatus", mInternetLinkStatus);
462             bundle.putInt("LinkDownBandwidthKbps", mLinkDownBandwidthKbps);
463             bundle.putInt("LinkUpBandwidthKbps", mLinkUpBandwidthKbps);
464             bundle.putInt("NetworkProbesResult", mNetworkProbesResult);
465             bundle.putInt("NetworkProbesType", mNetworkProbesType);
466             bundle.putInt("NetworkValidationResult", mNetworkValidationResult);
467             bundle.putInt("TcpMetricsCollectionPeriodMillis", mTcpMetricsCollectionPeriodMillis);
468             bundle.putInt("TcpPacketFailRate", mTcpPacketFailRate);
469             bundle.putInt("DnsConsecutiveTimeouts", mDnsConsecutiveTimeouts);
470         } else {
471             bundle.putInt("Action", action);
472             bundle.putBoolean("IsRecovered", isRecovered);
473             bundle.putInt("Duration", duration);
474             bundle.putInt("Reason", reason);
475             bundle.putBoolean("IsFirstValidation", validationCount == 1);
476             bundle.putInt("DurationOfAction", durationOfAction);
477             bundle.putInt("PhoneId", mPhoneId);
478             bundle.putInt("CarrierId", mCarrierId);
479             bundle.putInt("SignalStrength", mSignalStrength);
480             bundle.putInt("Band", mBand);
481             bundle.putInt("Rat", mRat);
482             bundle.putBoolean("IsOpportunistic", mIsOpportunistic);
483             bundle.putBoolean("IsMultiSim", mIsMultiSim);
484             bundle.putInt("NetworkRegState", mNetworkRegState);
485             bundle.putInt("OtherSignalStrength", mOtherSignalStrength);
486             bundle.putInt("OtherNetworkRegState", mOtherNetworkRegState);
487             bundle.putInt("InternetLinkStatus", mInternetLinkStatus);
488             bundle.putInt("LinkDownBandwidthKbps", mLinkDownBandwidthKbps);
489             bundle.putInt("LinkUpBandwidthKbps", mLinkUpBandwidthKbps);
490         }
491 
492         return bundle;
493     }
494 
log(@onNull String s)495     private void log(@NonNull String s) {
496         Rlog.i(mTag, s);
497     }
498 
logd(@onNull String s)499     private void logd(@NonNull String s) {
500         Rlog.d(mTag, s);
501     }
502 
loge(@onNull String s)503     private void loge(@NonNull String s) {
504         Rlog.e(mTag, s);
505     }
506 
loge(@onNull String s, Throwable tr)507     private void loge(@NonNull String s, Throwable tr) {
508         Rlog.e(mTag, s, tr);
509     }
510 
511     @Override
toString()512     public String toString() {
513         return "DataStallRecoveryStats {"
514             + "Phone id:"
515             + mPhoneId
516             + ", Signal strength:"
517             + mSignalStrength
518             + ", Band:" + mBand
519             + ", RAT:" + mRat
520             + ", Opportunistic:"
521             + mIsOpportunistic
522             + ", Multi-SIM:"
523             + mIsMultiSim
524             + ", Network reg state:"
525             + mNetworkRegState
526             + ", Other signal strength:"
527             + mOtherSignalStrength
528             + ", Other network reg state:"
529             + mOtherNetworkRegState
530             + ", Link status:"
531             + mInternetLinkStatus
532             + ", Link down bandwidth:"
533             + mLinkDownBandwidthKbps
534             + ", Link up bandwidth:"
535             + mLinkUpBandwidthKbps
536             + "}";
537     }
538 }
539