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