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