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