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