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