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