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