1 /*
2  * Copyright (C) 2008 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.location;
18 
19 import android.app.Notification;
20 import android.app.NotificationManager;
21 import android.app.PendingIntent;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.location.INetInitiatedListener;
27 import android.location.LocationManager;
28 import android.os.RemoteException;
29 import android.os.SystemClock;
30 import android.os.UserHandle;
31 import android.telephony.PhoneNumberUtils;
32 import android.telephony.PhoneStateListener;
33 import android.telephony.TelephonyManager;
34 import android.util.Log;
35 
36 import com.android.internal.R;
37 import com.android.internal.notification.SystemNotificationChannels;
38 import com.android.internal.telephony.GsmAlphabet;
39 
40 import java.io.UnsupportedEncodingException;
41 import java.util.concurrent.TimeUnit;
42 
43 /**
44  * A GPS Network-initiated Handler class used by LocationManager.
45  *
46  * {@hide}
47  */
48 public class GpsNetInitiatedHandler {
49 
50     private static final String TAG = "GpsNetInitiatedHandler";
51 
52     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
53 
54     // NI verify activity for bringing up UI (not used yet)
55     public static final String ACTION_NI_VERIFY = "android.intent.action.NETWORK_INITIATED_VERIFY";
56 
57     // string constants for defining data fields in NI Intent
58     public static final String NI_INTENT_KEY_NOTIF_ID = "notif_id";
59     public static final String NI_INTENT_KEY_TITLE = "title";
60     public static final String NI_INTENT_KEY_MESSAGE = "message";
61     public static final String NI_INTENT_KEY_TIMEOUT = "timeout";
62     public static final String NI_INTENT_KEY_DEFAULT_RESPONSE = "default_resp";
63 
64     // the extra command to send NI response to GnssLocationProvider
65     public static final String NI_RESPONSE_EXTRA_CMD = "send_ni_response";
66 
67     // the extra command parameter names in the Bundle
68     public static final String NI_EXTRA_CMD_NOTIF_ID = "notif_id";
69     public static final String NI_EXTRA_CMD_RESPONSE = "response";
70 
71     // these need to match GpsNiType constants in gps_ni.h
72     public static final int GPS_NI_TYPE_VOICE = 1;
73     public static final int GPS_NI_TYPE_UMTS_SUPL = 2;
74     public static final int GPS_NI_TYPE_UMTS_CTRL_PLANE = 3;
75     public static final int GPS_NI_TYPE_EMERGENCY_SUPL = 4;
76 
77     // these need to match GpsUserResponseType constants in gps_ni.h
78     public static final int GPS_NI_RESPONSE_ACCEPT = 1;
79     public static final int GPS_NI_RESPONSE_DENY = 2;
80     public static final int GPS_NI_RESPONSE_NORESP = 3;
81     public static final int GPS_NI_RESPONSE_IGNORE = 4;
82 
83     // these need to match GpsNiNotifyFlags constants in gps_ni.h
84     public static final int GPS_NI_NEED_NOTIFY = 0x0001;
85     public static final int GPS_NI_NEED_VERIFY = 0x0002;
86     public static final int GPS_NI_PRIVACY_OVERRIDE = 0x0004;
87 
88     // these need to match GpsNiEncodingType in gps_ni.h
89     public static final int GPS_ENC_NONE = 0;
90     public static final int GPS_ENC_SUPL_GSM_DEFAULT = 1;
91     public static final int GPS_ENC_SUPL_UTF8 = 2;
92     public static final int GPS_ENC_SUPL_UCS2 = 3;
93     public static final int GPS_ENC_UNKNOWN = -1;
94 
95     private final Context mContext;
96     private final TelephonyManager mTelephonyManager;
97     private final PhoneStateListener mPhoneStateListener;
98 
99     // parent gps location provider
100     private final LocationManager mLocationManager;
101 
102     // configuration of notificaiton behavior
103     private boolean mPlaySounds = false;
104     private boolean mPopupImmediately = true;
105 
106     // read the SUPL_ES form gps.conf
107     private volatile boolean mIsSuplEsEnabled;
108 
109     // Set to true if the phone is having emergency call.
110     private volatile boolean mIsInEmergencyCall;
111 
112     // If Location function is enabled.
113     private volatile boolean mIsLocationEnabled = false;
114 
115     private final INetInitiatedListener mNetInitiatedListener;
116 
117     // Set to true if string from HAL is encoded as Hex, e.g., "3F0039"
118     static private boolean mIsHexInput = true;
119 
120     // End time of emergency call, and extension, if set
121     private volatile long mCallEndElapsedRealtimeMillis = 0;
122     private volatile long mEmergencyExtensionMillis = 0;
123 
124     public static class GpsNiNotification
125     {
126         public int notificationId;
127         public int niType;
128         public boolean needNotify;
129         public boolean needVerify;
130         public boolean privacyOverride;
131         public int timeout;
132         public int defaultResponse;
133         public String requestorId;
134         public String text;
135         public int requestorIdEncoding;
136         public int textEncoding;
137     };
138 
139     public static class GpsNiResponse {
140         /* User response, one of the values in GpsUserResponseType */
141         int userResponse;
142     };
143 
144     private final BroadcastReceiver mBroadcastReciever = new BroadcastReceiver() {
145 
146         @Override public void onReceive(Context context, Intent intent) {
147             String action = intent.getAction();
148             if (action.equals(Intent.ACTION_NEW_OUTGOING_CALL)) {
149                 String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
150                 /*
151                    Tracks the emergency call:
152                        mIsInEmergencyCall records if the phone is in emergency call or not. It will
153                        be set to true when the phone is having emergency call, and then will
154                        be set to false by mPhoneStateListener when the emergency call ends.
155                 */
156                 mIsInEmergencyCall = PhoneNumberUtils.isEmergencyNumber(phoneNumber);
157                 if (DEBUG) Log.v(TAG, "ACTION_NEW_OUTGOING_CALL - " + getInEmergency());
158             } else if (action.equals(LocationManager.MODE_CHANGED_ACTION)) {
159                 updateLocationMode();
160                 if (DEBUG) Log.d(TAG, "location enabled :" + getLocationEnabled());
161             }
162         }
163     };
164 
165     /**
166      * The notification that is shown when a network-initiated notification
167      * (and verification) event is received.
168      * <p>
169      * This is lazily created, so use {@link #setNINotification()}.
170      */
171     private Notification.Builder mNiNotificationBuilder;
172 
GpsNetInitiatedHandler(Context context, INetInitiatedListener netInitiatedListener, boolean isSuplEsEnabled)173     public GpsNetInitiatedHandler(Context context,
174                                   INetInitiatedListener netInitiatedListener,
175                                   boolean isSuplEsEnabled) {
176         mContext = context;
177 
178         if (netInitiatedListener == null) {
179             throw new IllegalArgumentException("netInitiatedListener is null");
180         } else {
181             mNetInitiatedListener = netInitiatedListener;
182         }
183 
184         setSuplEsEnabled(isSuplEsEnabled);
185         mLocationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
186         updateLocationMode();
187         mTelephonyManager =
188             (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
189 
190         mPhoneStateListener = new PhoneStateListener() {
191             @Override
192             public void onCallStateChanged(int state, String incomingNumber) {
193                 if (DEBUG) Log.d(TAG, "onCallStateChanged(): state is "+ state);
194                 // listening for emergency call ends
195                 if (state == TelephonyManager.CALL_STATE_IDLE) {
196                     if (mIsInEmergencyCall) {
197                         mCallEndElapsedRealtimeMillis = SystemClock.elapsedRealtime();
198                         mIsInEmergencyCall = false;
199                     }
200                 }
201             }
202         };
203         mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
204 
205         IntentFilter intentFilter = new IntentFilter();
206         intentFilter.addAction(Intent.ACTION_NEW_OUTGOING_CALL);
207         intentFilter.addAction(LocationManager.MODE_CHANGED_ACTION);
208         mContext.registerReceiver(mBroadcastReciever, intentFilter);
209     }
210 
setSuplEsEnabled(boolean isEnabled)211     public void setSuplEsEnabled(boolean isEnabled) {
212         mIsSuplEsEnabled = isEnabled;
213     }
214 
getSuplEsEnabled()215     public boolean getSuplEsEnabled() {
216         return mIsSuplEsEnabled;
217     }
218 
219     /**
220      * Updates Location enabler based on location setting.
221      */
updateLocationMode()222     public void updateLocationMode() {
223         mIsLocationEnabled = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
224     }
225 
226     /**
227      * Checks if user agreed to use location.
228      */
getLocationEnabled()229     public boolean getLocationEnabled() {
230         return mIsLocationEnabled;
231     }
232 
233     /**
234      * Determines whether device is in user-initiated emergency session based on the following
235      * 1. If the user is making an emergency call, this is provided by actively
236      *    monitoring the outgoing phone number;
237      * 2. If the user has recently ended an emergency call, and the device is in a configured time
238      *    window after the end of that call.
239      * 3. If the device is in a emergency callback state, this is provided by querying
240      *    TelephonyManager.
241      * 4. If the user has recently sent an Emergency SMS and telephony reports that it is in
242      *    emergency SMS mode, this is provided by querying TelephonyManager.
243      * @return true if is considered in user initiated emergency mode for NI purposes
244      */
getInEmergency()245     public boolean getInEmergency() {
246         boolean isInEmergencyExtension =
247                 (mCallEndElapsedRealtimeMillis > 0)
248                 && ((SystemClock.elapsedRealtime() - mCallEndElapsedRealtimeMillis)
249                         < mEmergencyExtensionMillis);
250         boolean isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
251         boolean isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
252         return mIsInEmergencyCall || isInEmergencyCallback || isInEmergencyExtension
253                 || isInEmergencySmsMode;
254     }
255 
setEmergencyExtensionSeconds(int emergencyExtensionSeconds)256     public void setEmergencyExtensionSeconds(int emergencyExtensionSeconds) {
257         mEmergencyExtensionMillis = TimeUnit.SECONDS.toMillis(emergencyExtensionSeconds);
258     }
259 
260     // Handles NI events from HAL
handleNiNotification(GpsNiNotification notif)261     public void handleNiNotification(GpsNiNotification notif) {
262         if (DEBUG) Log.d(TAG, "in handleNiNotification () :"
263                         + " notificationId: " + notif.notificationId
264                         + " requestorId: " + notif.requestorId
265                         + " text: " + notif.text
266                         + " mIsSuplEsEnabled" + getSuplEsEnabled()
267                         + " mIsLocationEnabled" + getLocationEnabled());
268 
269         if (getSuplEsEnabled()) {
270             handleNiInEs(notif);
271         } else {
272             handleNi(notif);
273         }
274 
275         //////////////////////////////////////////////////////////////////////////
276         //   A note about timeout
277         //   According to the protocol, in the need_notify and need_verify case,
278         //   a default response should be sent when time out.
279         //
280         //   In some GPS hardware, the GPS driver (under HAL) can handle the timeout case
281         //   and this class GpsNetInitiatedHandler does not need to do anything.
282         //
283         //   However, the UI should at least close the dialog when timeout. Further,
284         //   for more general handling, timeout response should be added to the Handler here.
285         //
286     }
287 
288     // handle NI form HAL when SUPL_ES is disabled.
handleNi(GpsNiNotification notif)289     private void handleNi(GpsNiNotification notif) {
290         if (DEBUG) Log.d(TAG, "in handleNi () :"
291                         + " needNotify: " + notif.needNotify
292                         + " needVerify: " + notif.needVerify
293                         + " privacyOverride: " + notif.privacyOverride
294                         + " mPopupImmediately: " + mPopupImmediately
295                         + " mInEmergency: " + getInEmergency());
296 
297         if (!getLocationEnabled() && !getInEmergency()) {
298             // Location is currently disabled, ignore all NI requests.
299             try {
300                 mNetInitiatedListener.sendNiResponse(notif.notificationId,
301                                                      GPS_NI_RESPONSE_IGNORE);
302             } catch (RemoteException e) {
303                 Log.e(TAG, "RemoteException in sendNiResponse");
304             }
305         }
306         if (notif.needNotify) {
307         // If NI does not need verify or the dialog is not requested
308         // to pop up immediately, the dialog box will not pop up.
309             if (notif.needVerify && mPopupImmediately) {
310                 // Popup the dialog box now
311                 openNiDialog(notif);
312             } else {
313                 // Show the notification
314                 setNiNotification(notif);
315             }
316         }
317         // ACCEPT cases: 1. Notify, no verify; 2. no notify, no verify;
318         // 3. privacy override.
319         if (!notif.needVerify || notif.privacyOverride) {
320             try {
321                 mNetInitiatedListener.sendNiResponse(notif.notificationId,
322                                                      GPS_NI_RESPONSE_ACCEPT);
323             } catch (RemoteException e) {
324                 Log.e(TAG, "RemoteException in sendNiResponse");
325             }
326         }
327     }
328 
329     // handle NI from HAL when the SUPL_ES is enabled
handleNiInEs(GpsNiNotification notif)330     private void handleNiInEs(GpsNiNotification notif) {
331 
332         if (DEBUG) Log.d(TAG, "in handleNiInEs () :"
333                     + " niType: " + notif.niType
334                     + " notificationId: " + notif.notificationId);
335 
336         // UE is in emergency mode when in emergency call mode or in emergency call back mode
337         /*
338            1. When SUPL ES bit is off and UE is not in emergency mode:
339                   Call handleNi() to do legacy behaviour.
340            2. When SUPL ES bit is on and UE is in emergency mode:
341                   Call handleNi() to do acceptance behaviour.
342            3. When SUPL ES bit is off but UE is in emergency mode:
343                   Ignore the emergency SUPL INIT.
344            4. When SUPL ES bit is on but UE is not in emergency mode:
345                   Ignore the emergency SUPL INIT.
346         */
347         boolean isNiTypeES = (notif.niType == GPS_NI_TYPE_EMERGENCY_SUPL);
348         if (isNiTypeES != getInEmergency()) {
349             try {
350                 mNetInitiatedListener.sendNiResponse(notif.notificationId,
351                                                      GPS_NI_RESPONSE_IGNORE);
352             } catch (RemoteException e) {
353                 Log.e(TAG, "RemoteException in sendNiResponse");
354             }
355         } else {
356             handleNi(notif);
357         }
358     }
359 
360     /**
361      * Posts a notification in the status bar using the contents in {@code notif} object.
362      */
setNiNotification(GpsNiNotification notif)363     private synchronized void setNiNotification(GpsNiNotification notif) {
364         NotificationManager notificationManager = (NotificationManager) mContext
365                 .getSystemService(Context.NOTIFICATION_SERVICE);
366         if (notificationManager == null) {
367             return;
368         }
369 
370         String title = getNotifTitle(notif, mContext);
371         String message = getNotifMessage(notif, mContext);
372 
373         if (DEBUG) Log.d(TAG, "setNiNotification, notifyId: " + notif.notificationId +
374                 ", title: " + title +
375                 ", message: " + message);
376 
377         // Construct Notification
378         if (mNiNotificationBuilder == null) {
379             mNiNotificationBuilder = new Notification.Builder(mContext,
380                 SystemNotificationChannels.NETWORK_ALERTS)
381                     .setSmallIcon(com.android.internal.R.drawable.stat_sys_gps_on)
382                     .setWhen(0)
383                     .setOngoing(true)
384                     .setAutoCancel(true)
385                     .setColor(mContext.getColor(
386                             com.android.internal.R.color.system_notification_accent_color));
387         }
388 
389         if (mPlaySounds) {
390             mNiNotificationBuilder.setDefaults(Notification.DEFAULT_SOUND);
391         } else {
392             mNiNotificationBuilder.setDefaults(0);
393         }
394 
395         // if not to popup dialog immediately, pending intent will open the dialog
396         Intent intent = !mPopupImmediately ? getDlgIntent(notif) : new Intent();
397         PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0);
398         mNiNotificationBuilder.setTicker(getNotifTicker(notif, mContext))
399                 .setContentTitle(title)
400                 .setContentText(message)
401                 .setContentIntent(pi);
402 
403         notificationManager.notifyAsUser(null, notif.notificationId, mNiNotificationBuilder.build(),
404                 UserHandle.ALL);
405     }
406 
407     // Opens the notification dialog and waits for user input
openNiDialog(GpsNiNotification notif)408     private void openNiDialog(GpsNiNotification notif)
409     {
410         Intent intent = getDlgIntent(notif);
411 
412         if (DEBUG) Log.d(TAG, "openNiDialog, notifyId: " + notif.notificationId +
413                 ", requestorId: " + notif.requestorId +
414                 ", text: " + notif.text);
415 
416         mContext.startActivity(intent);
417     }
418 
419     // Construct the intent for bringing up the dialog activity, which shows the
420     // notification and takes user input
getDlgIntent(GpsNiNotification notif)421     private Intent getDlgIntent(GpsNiNotification notif)
422     {
423         Intent intent = new Intent();
424         String title = getDialogTitle(notif, mContext);
425         String message = getDialogMessage(notif, mContext);
426 
427         // directly bring up the NI activity
428         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
429         intent.setClass(mContext, com.android.internal.app.NetInitiatedActivity.class);
430 
431         // put data in the intent
432         intent.putExtra(NI_INTENT_KEY_NOTIF_ID, notif.notificationId);
433         intent.putExtra(NI_INTENT_KEY_TITLE, title);
434         intent.putExtra(NI_INTENT_KEY_MESSAGE, message);
435         intent.putExtra(NI_INTENT_KEY_TIMEOUT, notif.timeout);
436         intent.putExtra(NI_INTENT_KEY_DEFAULT_RESPONSE, notif.defaultResponse);
437 
438         if (DEBUG) Log.d(TAG, "generateIntent, title: " + title + ", message: " + message +
439                 ", timeout: " + notif.timeout);
440 
441         return intent;
442     }
443 
444     // Converts a string (or Hex string) to a char array
stringToByteArray(String original, boolean isHex)445     static byte[] stringToByteArray(String original, boolean isHex)
446     {
447         int length = isHex ? original.length() / 2 : original.length();
448         byte[] output = new byte[length];
449         int i;
450 
451         if (isHex)
452         {
453             for (i = 0; i < length; i++)
454             {
455                 output[i] = (byte) Integer.parseInt(original.substring(i*2, i*2+2), 16);
456             }
457         }
458         else {
459             for (i = 0; i < length; i++)
460             {
461                 output[i] = (byte) original.charAt(i);
462             }
463         }
464 
465         return output;
466     }
467 
468     /**
469      * Unpacks an byte array containing 7-bit packed characters into a String.
470      *
471      * @param input a 7-bit packed char array
472      * @return the unpacked String
473      */
decodeGSMPackedString(byte[] input)474     static String decodeGSMPackedString(byte[] input)
475     {
476         final char PADDING_CHAR = 0x00;
477         int lengthBytes = input.length;
478         int lengthSeptets = (lengthBytes * 8) / 7;
479         String decoded;
480 
481         /* Special case where the last 7 bits in the last byte could hold a valid
482          * 7-bit character or a padding character. Drop the last 7-bit character
483          * if it is a padding character.
484          */
485         if (lengthBytes % 7 == 0) {
486             if (lengthBytes > 0) {
487                 if ((input[lengthBytes - 1] >> 1) == PADDING_CHAR) {
488                     lengthSeptets = lengthSeptets - 1;
489                 }
490             }
491         }
492 
493         decoded = GsmAlphabet.gsm7BitPackedToString(input, 0, lengthSeptets);
494 
495         // Return "" if decoding of GSM packed string fails
496         if (null == decoded) {
497             Log.e(TAG, "Decoding of GSM packed string failed");
498             decoded = "";
499         }
500 
501         return decoded;
502     }
503 
decodeUTF8String(byte[] input)504     static String decodeUTF8String(byte[] input)
505     {
506         String decoded = "";
507         try {
508             decoded = new String(input, "UTF-8");
509         }
510         catch (UnsupportedEncodingException e)
511         {
512             throw new AssertionError();
513         }
514         return decoded;
515     }
516 
decodeUCS2String(byte[] input)517     static String decodeUCS2String(byte[] input)
518     {
519         String decoded = "";
520         try {
521             decoded = new String(input, "UTF-16");
522         }
523         catch (UnsupportedEncodingException e)
524         {
525             throw new AssertionError();
526         }
527         return decoded;
528     }
529 
530     /** Decode NI string
531      *
532      * @param original   The text string to be decoded
533      * @param isHex      Specifies whether the content of the string has been encoded as a Hex string. Encoding
534      *                   a string as Hex can allow zeros inside the coded text.
535      * @param coding     Specifies the coding scheme of the string, such as GSM, UTF8, UCS2, etc. This coding scheme
536      *                      needs to match those used passed to HAL from the native GPS driver. Decoding is done according
537      *                   to the <code> coding </code>, after a Hex string is decoded. Generally, if the
538      *                   notification strings don't need further decoding, <code> coding </code> encoding can be
539      *                   set to -1, and <code> isHex </code> can be false.
540      * @return the decoded string
541      */
decodeString(String original, boolean isHex, int coding)542     static private String decodeString(String original, boolean isHex, int coding)
543     {
544         if (coding == GPS_ENC_NONE || coding == GPS_ENC_UNKNOWN) {
545             return original;
546         }
547 
548         byte[] input = stringToByteArray(original, isHex);
549 
550         switch (coding) {
551             case GPS_ENC_SUPL_GSM_DEFAULT:
552                 return decodeGSMPackedString(input);
553 
554             case GPS_ENC_SUPL_UTF8:
555                 return decodeUTF8String(input);
556 
557             case GPS_ENC_SUPL_UCS2:
558                 return decodeUCS2String(input);
559 
560             default:
561                 Log.e(TAG, "Unknown encoding " + coding + " for NI text " + original);
562                 return original;
563         }
564     }
565 
566     // change this to configure notification display
getNotifTicker(GpsNiNotification notif, Context context)567     static private String getNotifTicker(GpsNiNotification notif, Context context)
568     {
569         String ticker = String.format(context.getString(R.string.gpsNotifTicker),
570                 decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding),
571                 decodeString(notif.text, mIsHexInput, notif.textEncoding));
572         return ticker;
573     }
574 
575     // change this to configure notification display
getNotifTitle(GpsNiNotification notif, Context context)576     static private String getNotifTitle(GpsNiNotification notif, Context context)
577     {
578         String title = String.format(context.getString(R.string.gpsNotifTitle));
579         return title;
580     }
581 
582     // change this to configure notification display
getNotifMessage(GpsNiNotification notif, Context context)583     static private String getNotifMessage(GpsNiNotification notif, Context context)
584     {
585         String message = String.format(context.getString(R.string.gpsNotifMessage),
586                 decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding),
587                 decodeString(notif.text, mIsHexInput, notif.textEncoding));
588         return message;
589     }
590 
591     // change this to configure dialog display (for verification)
getDialogTitle(GpsNiNotification notif, Context context)592     static public String getDialogTitle(GpsNiNotification notif, Context context)
593     {
594         return getNotifTitle(notif, context);
595     }
596 
597     // change this to configure dialog display (for verification)
getDialogMessage(GpsNiNotification notif, Context context)598     static private String getDialogMessage(GpsNiNotification notif, Context context)
599     {
600         return getNotifMessage(notif, context);
601     }
602 
603 }
604