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.Context; 25 import android.content.Intent; 26 import android.os.Bundle; 27 import android.os.RemoteException; 28 import android.util.Log; 29 30 /** 31 * A GPS Network-initiated Handler class used by LocationManager. 32 * 33 * {@hide} 34 */ 35 public class GpsNetInitiatedHandler { 36 37 private static final String TAG = "GpsNetInitiatedHandler"; 38 39 private static final boolean DEBUG = true; 40 private static final boolean VERBOSE = false; 41 42 // NI verify activity for bringing up UI (not used yet) 43 public static final String ACTION_NI_VERIFY = "android.intent.action.NETWORK_INITIATED_VERIFY"; 44 45 // string constants for defining data fields in NI Intent 46 public static final String NI_INTENT_KEY_NOTIF_ID = "notif_id"; 47 public static final String NI_INTENT_KEY_TITLE = "title"; 48 public static final String NI_INTENT_KEY_MESSAGE = "message"; 49 public static final String NI_INTENT_KEY_TIMEOUT = "timeout"; 50 public static final String NI_INTENT_KEY_DEFAULT_RESPONSE = "default_resp"; 51 52 // the extra command to send NI response to GpsLocationProvider 53 public static final String NI_RESPONSE_EXTRA_CMD = "send_ni_response"; 54 55 // the extra command parameter names in the Bundle 56 public static final String NI_EXTRA_CMD_NOTIF_ID = "notif_id"; 57 public static final String NI_EXTRA_CMD_RESPONSE = "response"; 58 59 // these need to match GpsNiType constants in gps_ni.h 60 public static final int GPS_NI_TYPE_VOICE = 1; 61 public static final int GPS_NI_TYPE_UMTS_SUPL = 2; 62 public static final int GPS_NI_TYPE_UMTS_CTRL_PLANE = 3; 63 64 // these need to match GpsUserResponseType constants in gps_ni.h 65 public static final int GPS_NI_RESPONSE_ACCEPT = 1; 66 public static final int GPS_NI_RESPONSE_DENY = 2; 67 public static final int GPS_NI_RESPONSE_NORESP = 3; 68 69 // these need to match GpsNiNotifyFlags constants in gps_ni.h 70 public static final int GPS_NI_NEED_NOTIFY = 0x0001; 71 public static final int GPS_NI_NEED_VERIFY = 0x0002; 72 public static final int GPS_NI_PRIVACY_OVERRIDE = 0x0004; 73 74 // these need to match GpsNiEncodingType in gps_ni.h 75 public static final int GPS_ENC_NONE = 0; 76 public static final int GPS_ENC_SUPL_GSM_DEFAULT = 1; 77 public static final int GPS_ENC_SUPL_UTF8 = 2; 78 public static final int GPS_ENC_SUPL_UCS2 = 3; 79 public static final int GPS_ENC_UNKNOWN = -1; 80 81 private final Context mContext; 82 83 // parent gps location provider 84 private final GpsLocationProvider mGpsLocationProvider; 85 86 // configuration of notificaiton behavior 87 private boolean mPlaySounds = false; 88 private boolean visible = true; 89 private boolean mPopupImmediately = true; 90 91 // Set to true if string from HAL is encoded as Hex, e.g., "3F0039" 92 static private boolean mIsHexInput = true; 93 94 public static class GpsNiNotification 95 { 96 int notificationId; 97 int niType; 98 boolean needNotify; 99 boolean needVerify; 100 boolean privacyOverride; 101 int timeout; 102 int defaultResponse; 103 String requestorId; 104 String text; 105 int requestorIdEncoding; 106 int textEncoding; 107 Bundle extras; 108 }; 109 110 public static class GpsNiResponse { 111 /* User reponse, one of the values in GpsUserResponseType */ 112 int userResponse; 113 /* Optional extra data to pass with the user response */ 114 Bundle extras; 115 }; 116 117 /** 118 * The notification that is shown when a network-initiated notification 119 * (and verification) event is received. 120 * <p> 121 * This is lazily created, so use {@link #setNINotification()}. 122 */ 123 private Notification mNiNotification; 124 125 public GpsNetInitiatedHandler(Context context, GpsLocationProvider gpsLocationProvider) { 126 mContext = context; 127 mGpsLocationProvider = gpsLocationProvider; 128 } 129 130 // Handles NI events from HAL 131 public void handleNiNotification(GpsNiNotification notif) 132 { 133 if (DEBUG) Log.d(TAG, "handleNiNotification" + " notificationId: " + notif.notificationId 134 + " requestorId: " + notif.requestorId + " text: " + notif.text); 135 136 // Notify and verify with immediate pop-up 137 if (notif.needNotify && notif.needVerify && mPopupImmediately) 138 { 139 // Popup the dialog box now 140 openNiDialog(notif); 141 } 142 143 // Notify only, or delayed pop-up (change mPopupImmediately to FALSE) 144 if (notif.needNotify && !notif.needVerify || 145 notif.needNotify && notif.needVerify && !mPopupImmediately) 146 { 147 // Show the notification 148 149 // if mPopupImmediately == FALSE and needVerify == TRUE, a dialog will be opened 150 // when the user opens the notification message 151 152 setNiNotification(notif); 153 } 154 155 // ACCEPT cases: 1. Notify, no verify; 2. no notify, no verify; 3. privacy override. 156 if ( notif.needNotify && !notif.needVerify || 157 !notif.needNotify && !notif.needVerify || 158 notif.privacyOverride) 159 { 160 try { 161 mGpsLocationProvider.getNetInitiatedListener().sendNiResponse(notif.notificationId, GPS_NI_RESPONSE_ACCEPT); 162 } 163 catch (RemoteException e) 164 { 165 Log.e(TAG, e.getMessage()); 166 } 167 } 168 169 ////////////////////////////////////////////////////////////////////////// 170 // A note about timeout 171 // According to the protocol, in the need_notify and need_verify case, 172 // a default response should be sent when time out. 173 // 174 // In some GPS hardware, the GPS driver (under HAL) can handle the timeout case 175 // and this class GpsNetInitiatedHandler does not need to do anything. 176 // 177 // However, the UI should at least close the dialog when timeout. Further, 178 // for more general handling, timeout response should be added to the Handler here. 179 // 180 } 181 182 // Sets the NI notification. 183 private synchronized void setNiNotification(GpsNiNotification notif) { 184 NotificationManager notificationManager = (NotificationManager) mContext 185 .getSystemService(Context.NOTIFICATION_SERVICE); 186 if (notificationManager == null) { 187 return; 188 } 189 190 String title = getNotifTitle(notif); 191 String message = getNotifMessage(notif); 192 193 if (DEBUG) Log.d(TAG, "setNiNotification, notifyId: " + notif.notificationId + 194 ", title: " + title + 195 ", message: " + message); 196 197 // Construct Notification 198 if (mNiNotification == null) { 199 mNiNotification = new Notification(); 200 mNiNotification.icon = com.android.internal.R.drawable.stat_sys_gps_on; /* Change notification icon here */ 201 mNiNotification.when = 0; 202 } 203 204 if (mPlaySounds) { 205 mNiNotification.defaults |= Notification.DEFAULT_SOUND; 206 } else { 207 mNiNotification.defaults &= ~Notification.DEFAULT_SOUND; 208 } 209 210 mNiNotification.flags = Notification.FLAG_ONGOING_EVENT; 211 mNiNotification.tickerText = getNotifTicker(notif); 212 213 // if not to popup dialog immediately, pending intent will open the dialog 214 Intent intent = !mPopupImmediately ? getDlgIntent(notif) : new Intent(); 215 PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0); 216 mNiNotification.setLatestEventInfo(mContext, title, message, pi); 217 218 if (visible) { 219 notificationManager.notify(notif.notificationId, mNiNotification); 220 } else { 221 notificationManager.cancel(notif.notificationId); 222 } 223 } 224 225 // Opens the notification dialog and waits for user input 226 private void openNiDialog(GpsNiNotification notif) 227 { 228 Intent intent = getDlgIntent(notif); 229 230 if (DEBUG) Log.d(TAG, "openNiDialog, notifyId: " + notif.notificationId + 231 ", requestorId: " + notif.requestorId + 232 ", text: " + notif.text); 233 234 mContext.startActivity(intent); 235 } 236 237 // Construct the intent for bringing up the dialog activity, which shows the 238 // notification and takes user input 239 private Intent getDlgIntent(GpsNiNotification notif) 240 { 241 Intent intent = new Intent(); 242 String title = getDialogTitle(notif); 243 String message = getDialogMessage(notif); 244 245 // directly bring up the NI activity 246 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 247 intent.setClass(mContext, com.android.internal.app.NetInitiatedActivity.class); 248 249 // put data in the intent 250 intent.putExtra(NI_INTENT_KEY_NOTIF_ID, notif.notificationId); 251 intent.putExtra(NI_INTENT_KEY_TITLE, title); 252 intent.putExtra(NI_INTENT_KEY_MESSAGE, message); 253 intent.putExtra(NI_INTENT_KEY_TIMEOUT, notif.timeout); 254 intent.putExtra(NI_INTENT_KEY_DEFAULT_RESPONSE, notif.defaultResponse); 255 256 if (DEBUG) Log.d(TAG, "generateIntent, title: " + title + ", message: " + message + 257 ", timeout: " + notif.timeout); 258 259 return intent; 260 } 261 262 // Converts a string (or Hex string) to a char array 263 static byte[] stringToByteArray(String original, boolean isHex) 264 { 265 int length = isHex ? original.length() / 2 : original.length(); 266 byte[] output = new byte[length]; 267 int i; 268 269 if (isHex) 270 { 271 for (i = 0; i < length; i++) 272 { 273 output[i] = (byte) Integer.parseInt(original.substring(i*2, i*2+2), 16); 274 } 275 } 276 else { 277 for (i = 0; i < length; i++) 278 { 279 output[i] = (byte) original.charAt(i); 280 } 281 } 282 283 return output; 284 } 285 286 /** 287 * Unpacks an byte array containing 7-bit packed characters into a String. 288 * 289 * @param input a 7-bit packed char array 290 * @return the unpacked String 291 */ 292 static String decodeGSMPackedString(byte[] input) 293 { 294 final char CHAR_CR = 0x0D; 295 int nStridx = 0; 296 int nPckidx = 0; 297 int num_bytes = input.length; 298 int cPrev = 0; 299 int cCurr = 0; 300 byte nShift; 301 byte nextChar; 302 byte[] stringBuf = new byte[input.length * 2]; 303 String result = ""; 304 305 while(nPckidx < num_bytes) 306 { 307 nShift = (byte) (nStridx & 0x07); 308 cCurr = input[nPckidx++]; 309 if (cCurr < 0) cCurr += 256; 310 311 /* A 7-bit character can be split at the most between two bytes of packed 312 ** data. 313 */ 314 nextChar = (byte) (( (cCurr << nShift) | (cPrev >> (8-nShift)) ) & 0x7F); 315 stringBuf[nStridx++] = nextChar; 316 317 /* Special case where the whole of the next 7-bit character fits inside 318 ** the current byte of packed data. 319 */ 320 if(nShift == 6) 321 { 322 /* If the next 7-bit character is a CR (0x0D) and it is the last 323 ** character, then it indicates a padding character. Drop it. 324 */ 325 if (nPckidx == num_bytes || (cCurr >> 1) == CHAR_CR) 326 { 327 break; 328 } 329 330 nextChar = (byte) (cCurr >> 1); 331 stringBuf[nStridx++] = nextChar; 332 } 333 334 cPrev = cCurr; 335 } 336 337 try{ 338 result = new String(stringBuf, 0, nStridx, "US-ASCII"); 339 } 340 catch (UnsupportedEncodingException e) 341 { 342 Log.e(TAG, e.getMessage()); 343 } 344 345 return result; 346 } 347 348 static String decodeUTF8String(byte[] input) 349 { 350 String decoded = ""; 351 try { 352 decoded = new String(input, "UTF-8"); 353 } 354 catch (UnsupportedEncodingException e) 355 { 356 Log.e(TAG, e.getMessage()); 357 } 358 return decoded; 359 } 360 361 static String decodeUCS2String(byte[] input) 362 { 363 String decoded = ""; 364 try { 365 decoded = new String(input, "UTF-16"); 366 } 367 catch (UnsupportedEncodingException e) 368 { 369 Log.e(TAG, e.getMessage()); 370 } 371 return decoded; 372 } 373 374 /** Decode NI string 375 * 376 * @param original The text string to be decoded 377 * @param isHex Specifies whether the content of the string has been encoded as a Hex string. Encoding 378 * a string as Hex can allow zeros inside the coded text. 379 * @param coding Specifies the coding scheme of the string, such as GSM, UTF8, UCS2, etc. This coding scheme 380 * needs to match those used passed to HAL from the native GPS driver. Decoding is done according 381 * to the <code> coding </code>, after a Hex string is decoded. Generally, if the 382 * notification strings don't need further decoding, <code> coding </code> encoding can be 383 * set to -1, and <code> isHex </code> can be false. 384 * @return the decoded string 385 */ 386 static private String decodeString(String original, boolean isHex, int coding) 387 { 388 String decoded = original; 389 byte[] input = stringToByteArray(original, isHex); 390 391 switch (coding) { 392 case GPS_ENC_NONE: 393 decoded = original; 394 break; 395 396 case GPS_ENC_SUPL_GSM_DEFAULT: 397 decoded = decodeGSMPackedString(input); 398 break; 399 400 case GPS_ENC_SUPL_UTF8: 401 decoded = decodeUTF8String(input); 402 break; 403 404 case GPS_ENC_SUPL_UCS2: 405 decoded = decodeUCS2String(input); 406 break; 407 408 case GPS_ENC_UNKNOWN: 409 decoded = original; 410 break; 411 412 default: 413 Log.e(TAG, "Unknown encoding " + coding + " for NI text " + original); 414 break; 415 } 416 return decoded; 417 } 418 419 // change this to configure notification display 420 static private String getNotifTicker(GpsNiNotification notif) 421 { 422 String ticker = String.format("Position request! ReqId: [%s] ClientName: [%s]", 423 decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding), 424 decodeString(notif.text, mIsHexInput, notif.textEncoding)); 425 return ticker; 426 } 427 428 // change this to configure notification display 429 static private String getNotifTitle(GpsNiNotification notif) 430 { 431 String title = String.format("Position Request"); 432 return title; 433 } 434 435 // change this to configure notification display 436 static private String getNotifMessage(GpsNiNotification notif) 437 { 438 String message = String.format( 439 "NI Request received from [%s] for client [%s]!", 440 decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding), 441 decodeString(notif.text, mIsHexInput, notif.textEncoding)); 442 return message; 443 } 444 445 // change this to configure dialog display (for verification) 446 static public String getDialogTitle(GpsNiNotification notif) 447 { 448 return getNotifTitle(notif); 449 } 450 451 // change this to configure dialog display (for verification) 452 static private String getDialogMessage(GpsNiNotification notif) 453 { 454 return getNotifMessage(notif); 455 } 456 457 } 458