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 java.io.UnsupportedEncodingException; 20 import java.util.concurrent.TimeUnit; 21 22 import android.app.Notification; 23 import android.app.NotificationManager; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.location.LocationManager; 29 import android.location.INetInitiatedListener; 30 import android.os.SystemClock; 31 import android.telephony.TelephonyManager; 32 import android.telephony.PhoneNumberUtils; 33 import android.telephony.PhoneStateListener; 34 import android.os.RemoteException; 35 import android.os.UserHandle; 36 import android.util.Log; 37 38 import com.android.internal.notification.SystemNotificationChannels; 39 import com.android.internal.R; 40 import com.android.internal.telephony.GsmAlphabet; 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 // Limit on SUPL NI emergency mode time extension after emergency sessions ends 95 private static final int MAX_EMERGENCY_MODE_EXTENSION_SECONDS = 300; // 5 minute maximum 96 97 private final Context mContext; 98 private final TelephonyManager mTelephonyManager; 99 private final PhoneStateListener mPhoneStateListener; 100 101 // parent gps location provider 102 private final LocationManager mLocationManager; 103 104 // configuration of notificaiton behavior 105 private boolean mPlaySounds = false; 106 private boolean mPopupImmediately = true; 107 108 // read the SUPL_ES form gps.conf 109 private volatile boolean mIsSuplEsEnabled; 110 111 // Set to true if the phone is having emergency call. 112 private volatile boolean mIsInEmergencyCall; 113 114 // If Location function is enabled. 115 private volatile boolean mIsLocationEnabled = false; 116 117 private final INetInitiatedListener mNetInitiatedListener; 118 119 // Set to true if string from HAL is encoded as Hex, e.g., "3F0039" 120 static private boolean mIsHexInput = true; 121 122 // End time of emergency call, and extension, if set 123 private volatile long mCallEndElapsedRealtimeMillis = 0; 124 private volatile long mEmergencyExtensionMillis = 0; 125 126 public static class GpsNiNotification 127 { 128 public int notificationId; 129 public int niType; 130 public boolean needNotify; 131 public boolean needVerify; 132 public boolean privacyOverride; 133 public int timeout; 134 public int defaultResponse; 135 public String requestorId; 136 public String text; 137 public int requestorIdEncoding; 138 public int textEncoding; 139 }; 140 141 public static class GpsNiResponse { 142 /* User response, one of the values in GpsUserResponseType */ 143 int userResponse; 144 }; 145 146 private final BroadcastReceiver mBroadcastReciever = new BroadcastReceiver() { 147 148 @Override public void onReceive(Context context, Intent intent) { 149 String action = intent.getAction(); 150 if (action.equals(Intent.ACTION_NEW_OUTGOING_CALL)) { 151 String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); 152 /* 153 Tracks the emergency call: 154 mIsInEmergencyCall records if the phone is in emergency call or not. It will 155 be set to true when the phone is having emergency call, and then will 156 be set to false by mPhoneStateListener when the emergency call ends. 157 */ 158 mIsInEmergencyCall = PhoneNumberUtils.isEmergencyNumber(phoneNumber); 159 if (DEBUG) Log.v(TAG, "ACTION_NEW_OUTGOING_CALL - " + getInEmergency()); 160 } else if (action.equals(LocationManager.MODE_CHANGED_ACTION)) { 161 updateLocationMode(); 162 if (DEBUG) Log.d(TAG, "location enabled :" + getLocationEnabled()); 163 } 164 } 165 }; 166 167 /** 168 * The notification that is shown when a network-initiated notification 169 * (and verification) event is received. 170 * <p> 171 * This is lazily created, so use {@link #setNINotification()}. 172 */ 173 private Notification.Builder mNiNotificationBuilder; 174 GpsNetInitiatedHandler(Context context, INetInitiatedListener netInitiatedListener, boolean isSuplEsEnabled)175 public GpsNetInitiatedHandler(Context context, 176 INetInitiatedListener netInitiatedListener, 177 boolean isSuplEsEnabled) { 178 mContext = context; 179 180 if (netInitiatedListener == null) { 181 throw new IllegalArgumentException("netInitiatedListener is null"); 182 } else { 183 mNetInitiatedListener = netInitiatedListener; 184 } 185 186 setSuplEsEnabled(isSuplEsEnabled); 187 mLocationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE); 188 updateLocationMode(); 189 mTelephonyManager = 190 (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); 191 192 mPhoneStateListener = new PhoneStateListener() { 193 @Override 194 public void onCallStateChanged(int state, String incomingNumber) { 195 if (DEBUG) Log.d(TAG, "onCallStateChanged(): state is "+ state); 196 // listening for emergency call ends 197 if (state == TelephonyManager.CALL_STATE_IDLE) { 198 if (mIsInEmergencyCall) { 199 mCallEndElapsedRealtimeMillis = SystemClock.elapsedRealtime(); 200 mIsInEmergencyCall = false; 201 } 202 } 203 } 204 }; 205 mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); 206 207 IntentFilter intentFilter = new IntentFilter(); 208 intentFilter.addAction(Intent.ACTION_NEW_OUTGOING_CALL); 209 intentFilter.addAction(LocationManager.MODE_CHANGED_ACTION); 210 mContext.registerReceiver(mBroadcastReciever, intentFilter); 211 } 212 setSuplEsEnabled(boolean isEnabled)213 public void setSuplEsEnabled(boolean isEnabled) { 214 mIsSuplEsEnabled = isEnabled; 215 } 216 getSuplEsEnabled()217 public boolean getSuplEsEnabled() { 218 return mIsSuplEsEnabled; 219 } 220 221 /** 222 * Updates Location enabler based on location setting. 223 */ updateLocationMode()224 public void updateLocationMode() { 225 mIsLocationEnabled = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); 226 } 227 228 /** 229 * Checks if user agreed to use location. 230 */ getLocationEnabled()231 public boolean getLocationEnabled() { 232 return mIsLocationEnabled; 233 } 234 235 /** 236 * Determines whether device is in user-initiated emergency session based on the following 237 * 1. If the user is making an emergency call, this is provided by actively 238 * monitoring the outgoing phone number; 239 * 2. If the user has recently ended an emergency call, and the device is in a configured time 240 * window after the end of that call. 241 * 3. If the device is in a emergency callback state, this is provided by querying 242 * 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 return mIsInEmergencyCall || isInEmergencyCallback || isInEmergencyExtension; 252 } 253 setEmergencyExtensionSeconds(int emergencyExtensionSeconds)254 public void setEmergencyExtensionSeconds(int emergencyExtensionSeconds) { 255 if (emergencyExtensionSeconds > MAX_EMERGENCY_MODE_EXTENSION_SECONDS) { 256 Log.w(TAG, "emergencyExtensionSeconds " + emergencyExtensionSeconds 257 + " too high, reset to " + MAX_EMERGENCY_MODE_EXTENSION_SECONDS); 258 emergencyExtensionSeconds = MAX_EMERGENCY_MODE_EXTENSION_SECONDS; 259 } else if (emergencyExtensionSeconds < 0) { 260 Log.w(TAG, "emergencyExtensionSeconds " + emergencyExtensionSeconds 261 + " is negative, reset to zero."); 262 emergencyExtensionSeconds = 0; 263 } 264 mEmergencyExtensionMillis = TimeUnit.SECONDS.toMillis(emergencyExtensionSeconds); 265 } 266 267 268 // Handles NI events from HAL handleNiNotification(GpsNiNotification notif)269 public void handleNiNotification(GpsNiNotification notif) { 270 if (DEBUG) Log.d(TAG, "in handleNiNotification () :" 271 + " notificationId: " + notif.notificationId 272 + " requestorId: " + notif.requestorId 273 + " text: " + notif.text 274 + " mIsSuplEsEnabled" + getSuplEsEnabled() 275 + " mIsLocationEnabled" + getLocationEnabled()); 276 277 if (getSuplEsEnabled()) { 278 handleNiInEs(notif); 279 } else { 280 handleNi(notif); 281 } 282 283 ////////////////////////////////////////////////////////////////////////// 284 // A note about timeout 285 // According to the protocol, in the need_notify and need_verify case, 286 // a default response should be sent when time out. 287 // 288 // In some GPS hardware, the GPS driver (under HAL) can handle the timeout case 289 // and this class GpsNetInitiatedHandler does not need to do anything. 290 // 291 // However, the UI should at least close the dialog when timeout. Further, 292 // for more general handling, timeout response should be added to the Handler here. 293 // 294 } 295 296 // handle NI form HAL when SUPL_ES is disabled. handleNi(GpsNiNotification notif)297 private void handleNi(GpsNiNotification notif) { 298 if (DEBUG) Log.d(TAG, "in handleNi () :" 299 + " needNotify: " + notif.needNotify 300 + " needVerify: " + notif.needVerify 301 + " privacyOverride: " + notif.privacyOverride 302 + " mPopupImmediately: " + mPopupImmediately 303 + " mInEmergency: " + getInEmergency()); 304 305 if (!getLocationEnabled() && !getInEmergency()) { 306 // Location is currently disabled, ignore all NI requests. 307 try { 308 mNetInitiatedListener.sendNiResponse(notif.notificationId, 309 GPS_NI_RESPONSE_IGNORE); 310 } catch (RemoteException e) { 311 Log.e(TAG, "RemoteException in sendNiResponse"); 312 } 313 } 314 if (notif.needNotify) { 315 // If NI does not need verify or the dialog is not requested 316 // to pop up immediately, the dialog box will not pop up. 317 if (notif.needVerify && mPopupImmediately) { 318 // Popup the dialog box now 319 openNiDialog(notif); 320 } else { 321 // Show the notification 322 setNiNotification(notif); 323 } 324 } 325 // ACCEPT cases: 1. Notify, no verify; 2. no notify, no verify; 326 // 3. privacy override. 327 if (!notif.needVerify || notif.privacyOverride) { 328 try { 329 mNetInitiatedListener.sendNiResponse(notif.notificationId, 330 GPS_NI_RESPONSE_ACCEPT); 331 } catch (RemoteException e) { 332 Log.e(TAG, "RemoteException in sendNiResponse"); 333 } 334 } 335 } 336 337 // handle NI from HAL when the SUPL_ES is enabled handleNiInEs(GpsNiNotification notif)338 private void handleNiInEs(GpsNiNotification notif) { 339 340 if (DEBUG) Log.d(TAG, "in handleNiInEs () :" 341 + " niType: " + notif.niType 342 + " notificationId: " + notif.notificationId); 343 344 // UE is in emergency mode when in emergency call mode or in emergency call back mode 345 /* 346 1. When SUPL ES bit is off and UE is not in emergency mode: 347 Call handleNi() to do legacy behaviour. 348 2. When SUPL ES bit is on and UE is in emergency mode: 349 Call handleNi() to do acceptance behaviour. 350 3. When SUPL ES bit is off but UE is in emergency mode: 351 Ignore the emergency SUPL INIT. 352 4. When SUPL ES bit is on but UE is not in emergency mode: 353 Ignore the emergency SUPL INIT. 354 */ 355 boolean isNiTypeES = (notif.niType == GPS_NI_TYPE_EMERGENCY_SUPL); 356 if (isNiTypeES != getInEmergency()) { 357 try { 358 mNetInitiatedListener.sendNiResponse(notif.notificationId, 359 GPS_NI_RESPONSE_IGNORE); 360 } catch (RemoteException e) { 361 Log.e(TAG, "RemoteException in sendNiResponse"); 362 } 363 } else { 364 handleNi(notif); 365 } 366 } 367 368 // Sets the NI notification. 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 */ decodeString(String original, boolean isHex, int coding)544 static private String decodeString(String original, boolean isHex, int coding) 545 { 546 String decoded = original; 547 byte[] input = stringToByteArray(original, isHex); 548 549 switch (coding) { 550 case GPS_ENC_NONE: 551 decoded = original; 552 break; 553 554 case GPS_ENC_SUPL_GSM_DEFAULT: 555 decoded = decodeGSMPackedString(input); 556 break; 557 558 case GPS_ENC_SUPL_UTF8: 559 decoded = decodeUTF8String(input); 560 break; 561 562 case GPS_ENC_SUPL_UCS2: 563 decoded = decodeUCS2String(input); 564 break; 565 566 case GPS_ENC_UNKNOWN: 567 decoded = original; 568 break; 569 570 default: 571 Log.e(TAG, "Unknown encoding " + coding + " for NI text " + original); 572 break; 573 } 574 return decoded; 575 } 576 577 // change this to configure notification display getNotifTicker(GpsNiNotification notif, Context context)578 static private String getNotifTicker(GpsNiNotification notif, Context context) 579 { 580 String ticker = String.format(context.getString(R.string.gpsNotifTicker), 581 decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding), 582 decodeString(notif.text, mIsHexInput, notif.textEncoding)); 583 return ticker; 584 } 585 586 // change this to configure notification display getNotifTitle(GpsNiNotification notif, Context context)587 static private String getNotifTitle(GpsNiNotification notif, Context context) 588 { 589 String title = String.format(context.getString(R.string.gpsNotifTitle)); 590 return title; 591 } 592 593 // change this to configure notification display getNotifMessage(GpsNiNotification notif, Context context)594 static private String getNotifMessage(GpsNiNotification notif, Context context) 595 { 596 String message = String.format(context.getString(R.string.gpsNotifMessage), 597 decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding), 598 decodeString(notif.text, mIsHexInput, notif.textEncoding)); 599 return message; 600 } 601 602 // change this to configure dialog display (for verification) getDialogTitle(GpsNiNotification notif, Context context)603 static public String getDialogTitle(GpsNiNotification notif, Context context) 604 { 605 return getNotifTitle(notif, context); 606 } 607 608 // change this to configure dialog display (for verification) getDialogMessage(GpsNiNotification notif, Context context)609 static private String getDialogMessage(GpsNiNotification notif, Context context) 610 { 611 return getNotifMessage(notif, context); 612 } 613 614 } 615