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