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