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.notification.SystemNotificationChannels; 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 GnssLocationProvider 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 }; 136 137 public static class GpsNiResponse { 138 /* User response, one of the values in GpsUserResponseType */ 139 int userResponse; 140 }; 141 142 private final BroadcastReceiver mBroadcastReciever = new BroadcastReceiver() { 143 144 @Override public void onReceive(Context context, Intent intent) { 145 String action = intent.getAction(); 146 if (action.equals(Intent.ACTION_NEW_OUTGOING_CALL)) { 147 String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); 148 /* 149 Emergency Mode is when during emergency call or in emergency call back mode. 150 For checking if it is during emergency call: 151 mIsInEmergency 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 For checking if it is in emergency call back mode: 155 Emergency call back mode will be checked by reading system properties 156 when necessary: SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE) 157 */ 158 setInEmergency(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 setInEmergency(false); 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 // Note: Currently, there are two mechanisms involved to determine if a 233 // phone is in emergency mode: 234 // 1. If the user is making an emergency call, this is provided by activly 235 // monitoring the outgoing phone number; 236 // 2. If the device is in a emergency callback state, this is provided by 237 // system properties. 238 // If either one of above exists, the phone is considered in an emergency 239 // mode. Because of this complexity, we need to be careful about how to set 240 // and clear the emergency state. setInEmergency(boolean isInEmergency)241 public void setInEmergency(boolean isInEmergency) { 242 mIsInEmergency = isInEmergency; 243 } 244 getInEmergency()245 public boolean getInEmergency() { 246 boolean isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode(); 247 return mIsInEmergency || isInEmergencyCallback; 248 } 249 250 251 // Handles NI events from HAL handleNiNotification(GpsNiNotification notif)252 public void handleNiNotification(GpsNiNotification notif) { 253 if (DEBUG) Log.d(TAG, "in handleNiNotification () :" 254 + " notificationId: " + notif.notificationId 255 + " requestorId: " + notif.requestorId 256 + " text: " + notif.text 257 + " mIsSuplEsEnabled" + getSuplEsEnabled() 258 + " mIsLocationEnabled" + getLocationEnabled()); 259 260 if (getSuplEsEnabled()) { 261 handleNiInEs(notif); 262 } else { 263 handleNi(notif); 264 } 265 266 ////////////////////////////////////////////////////////////////////////// 267 // A note about timeout 268 // According to the protocol, in the need_notify and need_verify case, 269 // a default response should be sent when time out. 270 // 271 // In some GPS hardware, the GPS driver (under HAL) can handle the timeout case 272 // and this class GpsNetInitiatedHandler does not need to do anything. 273 // 274 // However, the UI should at least close the dialog when timeout. Further, 275 // for more general handling, timeout response should be added to the Handler here. 276 // 277 } 278 279 // handle NI form HAL when SUPL_ES is disabled. handleNi(GpsNiNotification notif)280 private void handleNi(GpsNiNotification notif) { 281 if (DEBUG) Log.d(TAG, "in handleNi () :" 282 + " needNotify: " + notif.needNotify 283 + " needVerify: " + notif.needVerify 284 + " privacyOverride: " + notif.privacyOverride 285 + " mPopupImmediately: " + mPopupImmediately 286 + " mInEmergency: " + getInEmergency()); 287 288 if (!getLocationEnabled() && !getInEmergency()) { 289 // Location is currently disabled, ignore all NI requests. 290 try { 291 mNetInitiatedListener.sendNiResponse(notif.notificationId, 292 GPS_NI_RESPONSE_IGNORE); 293 } catch (RemoteException e) { 294 Log.e(TAG, "RemoteException in sendNiResponse"); 295 } 296 } 297 if (notif.needNotify) { 298 // If NI does not need verify or the dialog is not requested 299 // to pop up immediately, the dialog box will not pop up. 300 if (notif.needVerify && mPopupImmediately) { 301 // Popup the dialog box now 302 openNiDialog(notif); 303 } else { 304 // Show the notification 305 setNiNotification(notif); 306 } 307 } 308 // ACCEPT cases: 1. Notify, no verify; 2. no notify, no verify; 309 // 3. privacy override. 310 if (!notif.needVerify || notif.privacyOverride) { 311 try { 312 mNetInitiatedListener.sendNiResponse(notif.notificationId, 313 GPS_NI_RESPONSE_ACCEPT); 314 } catch (RemoteException e) { 315 Log.e(TAG, "RemoteException in sendNiResponse"); 316 } 317 } 318 } 319 320 // handle NI from HAL when the SUPL_ES is enabled handleNiInEs(GpsNiNotification notif)321 private void handleNiInEs(GpsNiNotification notif) { 322 323 if (DEBUG) Log.d(TAG, "in handleNiInEs () :" 324 + " niType: " + notif.niType 325 + " notificationId: " + notif.notificationId); 326 327 // UE is in emergency mode when in emergency call mode or in emergency call back mode 328 /* 329 1. When SUPL ES bit is off and UE is not in emergency mode: 330 Call handleNi() to do legacy behaviour. 331 2. When SUPL ES bit is on and UE is in emergency mode: 332 Call handleNi() to do acceptance behaviour. 333 3. When SUPL ES bit is off but UE is in emergency mode: 334 Ignore the emergency SUPL INIT. 335 4. When SUPL ES bit is on but UE is not in emergency mode: 336 Ignore the emergency SUPL INIT. 337 */ 338 boolean isNiTypeES = (notif.niType == GPS_NI_TYPE_EMERGENCY_SUPL); 339 if (isNiTypeES != getInEmergency()) { 340 try { 341 mNetInitiatedListener.sendNiResponse(notif.notificationId, 342 GPS_NI_RESPONSE_IGNORE); 343 } catch (RemoteException e) { 344 Log.e(TAG, "RemoteException in sendNiResponse"); 345 } 346 } else { 347 handleNi(notif); 348 } 349 } 350 351 // Sets the NI notification. setNiNotification(GpsNiNotification notif)352 private synchronized void setNiNotification(GpsNiNotification notif) { 353 NotificationManager notificationManager = (NotificationManager) mContext 354 .getSystemService(Context.NOTIFICATION_SERVICE); 355 if (notificationManager == null) { 356 return; 357 } 358 359 String title = getNotifTitle(notif, mContext); 360 String message = getNotifMessage(notif, mContext); 361 362 if (DEBUG) Log.d(TAG, "setNiNotification, notifyId: " + notif.notificationId + 363 ", title: " + title + 364 ", message: " + message); 365 366 // Construct Notification 367 if (mNiNotificationBuilder == null) { 368 mNiNotificationBuilder = new Notification.Builder(mContext, 369 SystemNotificationChannels.NETWORK_ALERTS) 370 .setSmallIcon(com.android.internal.R.drawable.stat_sys_gps_on) 371 .setWhen(0) 372 .setOngoing(true) 373 .setAutoCancel(true) 374 .setColor(mContext.getColor( 375 com.android.internal.R.color.system_notification_accent_color)); 376 } 377 378 if (mPlaySounds) { 379 mNiNotificationBuilder.setDefaults(Notification.DEFAULT_SOUND); 380 } else { 381 mNiNotificationBuilder.setDefaults(0); 382 } 383 384 // if not to popup dialog immediately, pending intent will open the dialog 385 Intent intent = !mPopupImmediately ? getDlgIntent(notif) : new Intent(); 386 PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0); 387 mNiNotificationBuilder.setTicker(getNotifTicker(notif, mContext)) 388 .setContentTitle(title) 389 .setContentText(message) 390 .setContentIntent(pi); 391 392 notificationManager.notifyAsUser(null, notif.notificationId, mNiNotificationBuilder.build(), 393 UserHandle.ALL); 394 } 395 396 // Opens the notification dialog and waits for user input openNiDialog(GpsNiNotification notif)397 private void openNiDialog(GpsNiNotification notif) 398 { 399 Intent intent = getDlgIntent(notif); 400 401 if (DEBUG) Log.d(TAG, "openNiDialog, notifyId: " + notif.notificationId + 402 ", requestorId: " + notif.requestorId + 403 ", text: " + notif.text); 404 405 mContext.startActivity(intent); 406 } 407 408 // Construct the intent for bringing up the dialog activity, which shows the 409 // notification and takes user input getDlgIntent(GpsNiNotification notif)410 private Intent getDlgIntent(GpsNiNotification notif) 411 { 412 Intent intent = new Intent(); 413 String title = getDialogTitle(notif, mContext); 414 String message = getDialogMessage(notif, mContext); 415 416 // directly bring up the NI activity 417 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 418 intent.setClass(mContext, com.android.internal.app.NetInitiatedActivity.class); 419 420 // put data in the intent 421 intent.putExtra(NI_INTENT_KEY_NOTIF_ID, notif.notificationId); 422 intent.putExtra(NI_INTENT_KEY_TITLE, title); 423 intent.putExtra(NI_INTENT_KEY_MESSAGE, message); 424 intent.putExtra(NI_INTENT_KEY_TIMEOUT, notif.timeout); 425 intent.putExtra(NI_INTENT_KEY_DEFAULT_RESPONSE, notif.defaultResponse); 426 427 if (DEBUG) Log.d(TAG, "generateIntent, title: " + title + ", message: " + message + 428 ", timeout: " + notif.timeout); 429 430 return intent; 431 } 432 433 // Converts a string (or Hex string) to a char array stringToByteArray(String original, boolean isHex)434 static byte[] stringToByteArray(String original, boolean isHex) 435 { 436 int length = isHex ? original.length() / 2 : original.length(); 437 byte[] output = new byte[length]; 438 int i; 439 440 if (isHex) 441 { 442 for (i = 0; i < length; i++) 443 { 444 output[i] = (byte) Integer.parseInt(original.substring(i*2, i*2+2), 16); 445 } 446 } 447 else { 448 for (i = 0; i < length; i++) 449 { 450 output[i] = (byte) original.charAt(i); 451 } 452 } 453 454 return output; 455 } 456 457 /** 458 * Unpacks an byte array containing 7-bit packed characters into a String. 459 * 460 * @param input a 7-bit packed char array 461 * @return the unpacked String 462 */ decodeGSMPackedString(byte[] input)463 static String decodeGSMPackedString(byte[] input) 464 { 465 final char PADDING_CHAR = 0x00; 466 int lengthBytes = input.length; 467 int lengthSeptets = (lengthBytes * 8) / 7; 468 String decoded; 469 470 /* Special case where the last 7 bits in the last byte could hold a valid 471 * 7-bit character or a padding character. Drop the last 7-bit character 472 * if it is a padding character. 473 */ 474 if (lengthBytes % 7 == 0) { 475 if (lengthBytes > 0) { 476 if ((input[lengthBytes - 1] >> 1) == PADDING_CHAR) { 477 lengthSeptets = lengthSeptets - 1; 478 } 479 } 480 } 481 482 decoded = GsmAlphabet.gsm7BitPackedToString(input, 0, lengthSeptets); 483 484 // Return "" if decoding of GSM packed string fails 485 if (null == decoded) { 486 Log.e(TAG, "Decoding of GSM packed string failed"); 487 decoded = ""; 488 } 489 490 return decoded; 491 } 492 decodeUTF8String(byte[] input)493 static String decodeUTF8String(byte[] input) 494 { 495 String decoded = ""; 496 try { 497 decoded = new String(input, "UTF-8"); 498 } 499 catch (UnsupportedEncodingException e) 500 { 501 throw new AssertionError(); 502 } 503 return decoded; 504 } 505 decodeUCS2String(byte[] input)506 static String decodeUCS2String(byte[] input) 507 { 508 String decoded = ""; 509 try { 510 decoded = new String(input, "UTF-16"); 511 } 512 catch (UnsupportedEncodingException e) 513 { 514 throw new AssertionError(); 515 } 516 return decoded; 517 } 518 519 /** Decode NI string 520 * 521 * @param original The text string to be decoded 522 * @param isHex Specifies whether the content of the string has been encoded as a Hex string. Encoding 523 * a string as Hex can allow zeros inside the coded text. 524 * @param coding Specifies the coding scheme of the string, such as GSM, UTF8, UCS2, etc. This coding scheme 525 * needs to match those used passed to HAL from the native GPS driver. Decoding is done according 526 * to the <code> coding </code>, after a Hex string is decoded. Generally, if the 527 * notification strings don't need further decoding, <code> coding </code> encoding can be 528 * set to -1, and <code> isHex </code> can be false. 529 * @return the decoded string 530 */ decodeString(String original, boolean isHex, int coding)531 static private String decodeString(String original, boolean isHex, int coding) 532 { 533 String decoded = original; 534 byte[] input = stringToByteArray(original, isHex); 535 536 switch (coding) { 537 case GPS_ENC_NONE: 538 decoded = original; 539 break; 540 541 case GPS_ENC_SUPL_GSM_DEFAULT: 542 decoded = decodeGSMPackedString(input); 543 break; 544 545 case GPS_ENC_SUPL_UTF8: 546 decoded = decodeUTF8String(input); 547 break; 548 549 case GPS_ENC_SUPL_UCS2: 550 decoded = decodeUCS2String(input); 551 break; 552 553 case GPS_ENC_UNKNOWN: 554 decoded = original; 555 break; 556 557 default: 558 Log.e(TAG, "Unknown encoding " + coding + " for NI text " + original); 559 break; 560 } 561 return decoded; 562 } 563 564 // change this to configure notification display getNotifTicker(GpsNiNotification notif, Context context)565 static private String getNotifTicker(GpsNiNotification notif, Context context) 566 { 567 String ticker = String.format(context.getString(R.string.gpsNotifTicker), 568 decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding), 569 decodeString(notif.text, mIsHexInput, notif.textEncoding)); 570 return ticker; 571 } 572 573 // change this to configure notification display getNotifTitle(GpsNiNotification notif, Context context)574 static private String getNotifTitle(GpsNiNotification notif, Context context) 575 { 576 String title = String.format(context.getString(R.string.gpsNotifTitle)); 577 return title; 578 } 579 580 // change this to configure notification display getNotifMessage(GpsNiNotification notif, Context context)581 static private String getNotifMessage(GpsNiNotification notif, Context context) 582 { 583 String message = String.format(context.getString(R.string.gpsNotifMessage), 584 decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding), 585 decodeString(notif.text, mIsHexInput, notif.textEncoding)); 586 return message; 587 } 588 589 // change this to configure dialog display (for verification) getDialogTitle(GpsNiNotification notif, Context context)590 static public String getDialogTitle(GpsNiNotification notif, Context context) 591 { 592 return getNotifTitle(notif, context); 593 } 594 595 // change this to configure dialog display (for verification) getDialogMessage(GpsNiNotification notif, Context context)596 static private String getDialogMessage(GpsNiNotification notif, Context context) 597 { 598 return getNotifMessage(notif, context); 599 } 600 601 } 602