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.os.UserHandle; 30 import android.util.Log; 31 32 import com.android.internal.R; 33 import com.android.internal.telephony.GsmAlphabet; 34 35 /** 36 * A GPS Network-initiated Handler class used by LocationManager. 37 * 38 * {@hide} 39 */ 40 public class GpsNetInitiatedHandler { 41 42 private static final String TAG = "GpsNetInitiatedHandler"; 43 44 private static final boolean DEBUG = true; 45 private static final boolean VERBOSE = false; 46 47 // NI verify activity for bringing up UI (not used yet) 48 public static final String ACTION_NI_VERIFY = "android.intent.action.NETWORK_INITIATED_VERIFY"; 49 50 // string constants for defining data fields in NI Intent 51 public static final String NI_INTENT_KEY_NOTIF_ID = "notif_id"; 52 public static final String NI_INTENT_KEY_TITLE = "title"; 53 public static final String NI_INTENT_KEY_MESSAGE = "message"; 54 public static final String NI_INTENT_KEY_TIMEOUT = "timeout"; 55 public static final String NI_INTENT_KEY_DEFAULT_RESPONSE = "default_resp"; 56 57 // the extra command to send NI response to GpsLocationProvider 58 public static final String NI_RESPONSE_EXTRA_CMD = "send_ni_response"; 59 60 // the extra command parameter names in the Bundle 61 public static final String NI_EXTRA_CMD_NOTIF_ID = "notif_id"; 62 public static final String NI_EXTRA_CMD_RESPONSE = "response"; 63 64 // these need to match GpsNiType constants in gps_ni.h 65 public static final int GPS_NI_TYPE_VOICE = 1; 66 public static final int GPS_NI_TYPE_UMTS_SUPL = 2; 67 public static final int GPS_NI_TYPE_UMTS_CTRL_PLANE = 3; 68 69 // these need to match GpsUserResponseType constants in gps_ni.h 70 public static final int GPS_NI_RESPONSE_ACCEPT = 1; 71 public static final int GPS_NI_RESPONSE_DENY = 2; 72 public static final int GPS_NI_RESPONSE_NORESP = 3; 73 74 // these need to match GpsNiNotifyFlags constants in gps_ni.h 75 public static final int GPS_NI_NEED_NOTIFY = 0x0001; 76 public static final int GPS_NI_NEED_VERIFY = 0x0002; 77 public static final int GPS_NI_PRIVACY_OVERRIDE = 0x0004; 78 79 // these need to match GpsNiEncodingType in gps_ni.h 80 public static final int GPS_ENC_NONE = 0; 81 public static final int GPS_ENC_SUPL_GSM_DEFAULT = 1; 82 public static final int GPS_ENC_SUPL_UTF8 = 2; 83 public static final int GPS_ENC_SUPL_UCS2 = 3; 84 public static final int GPS_ENC_UNKNOWN = -1; 85 86 private final Context mContext; 87 88 // parent gps location provider 89 private final LocationManager mLocationManager; 90 91 // configuration of notificaiton behavior 92 private boolean mPlaySounds = false; 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 notificationManager.notifyAsUser(null, notif.notificationId, mNiNotification, 217 UserHandle.ALL); 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, mContext); 238 String message = getDialogMessage(notif, mContext); 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 PADDING_CHAR = 0x00; 290 int lengthBytes = input.length; 291 int lengthSeptets = (lengthBytes * 8) / 7; 292 String decoded; 293 294 /* Special case where the last 7 bits in the last byte could hold a valid 295 * 7-bit character or a padding character. Drop the last 7-bit character 296 * if it is a padding character. 297 */ 298 if (lengthBytes % 7 == 0) { 299 if (lengthBytes > 0) { 300 if ((input[lengthBytes - 1] >> 1) == PADDING_CHAR) { 301 lengthSeptets = lengthSeptets - 1; 302 } 303 } 304 } 305 306 decoded = GsmAlphabet.gsm7BitPackedToString(input, 0, lengthSeptets); 307 308 // Return "" if decoding of GSM packed string fails 309 if (null == decoded) { 310 Log.e(TAG, "Decoding of GSM packed string failed"); 311 decoded = ""; 312 } 313 314 return decoded; 315 } 316 317 static String decodeUTF8String(byte[] input) 318 { 319 String decoded = ""; 320 try { 321 decoded = new String(input, "UTF-8"); 322 } 323 catch (UnsupportedEncodingException e) 324 { 325 throw new AssertionError(); 326 } 327 return decoded; 328 } 329 330 static String decodeUCS2String(byte[] input) 331 { 332 String decoded = ""; 333 try { 334 decoded = new String(input, "UTF-16"); 335 } 336 catch (UnsupportedEncodingException e) 337 { 338 throw new AssertionError(); 339 } 340 return decoded; 341 } 342 343 /** Decode NI string 344 * 345 * @param original The text string to be decoded 346 * @param isHex Specifies whether the content of the string has been encoded as a Hex string. Encoding 347 * a string as Hex can allow zeros inside the coded text. 348 * @param coding Specifies the coding scheme of the string, such as GSM, UTF8, UCS2, etc. This coding scheme 349 * needs to match those used passed to HAL from the native GPS driver. Decoding is done according 350 * to the <code> coding </code>, after a Hex string is decoded. Generally, if the 351 * notification strings don't need further decoding, <code> coding </code> encoding can be 352 * set to -1, and <code> isHex </code> can be false. 353 * @return the decoded string 354 */ 355 static private String decodeString(String original, boolean isHex, int coding) 356 { 357 String decoded = original; 358 byte[] input = stringToByteArray(original, isHex); 359 360 switch (coding) { 361 case GPS_ENC_NONE: 362 decoded = original; 363 break; 364 365 case GPS_ENC_SUPL_GSM_DEFAULT: 366 decoded = decodeGSMPackedString(input); 367 break; 368 369 case GPS_ENC_SUPL_UTF8: 370 decoded = decodeUTF8String(input); 371 break; 372 373 case GPS_ENC_SUPL_UCS2: 374 decoded = decodeUCS2String(input); 375 break; 376 377 case GPS_ENC_UNKNOWN: 378 decoded = original; 379 break; 380 381 default: 382 Log.e(TAG, "Unknown encoding " + coding + " for NI text " + original); 383 break; 384 } 385 return decoded; 386 } 387 388 // change this to configure notification display 389 static private String getNotifTicker(GpsNiNotification notif, Context context) 390 { 391 String ticker = String.format(context.getString(R.string.gpsNotifTicker), 392 decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding), 393 decodeString(notif.text, mIsHexInput, notif.textEncoding)); 394 return ticker; 395 } 396 397 // change this to configure notification display 398 static private String getNotifTitle(GpsNiNotification notif, Context context) 399 { 400 String title = String.format(context.getString(R.string.gpsNotifTitle)); 401 return title; 402 } 403 404 // change this to configure notification display 405 static private String getNotifMessage(GpsNiNotification notif, Context context) 406 { 407 String message = String.format(context.getString(R.string.gpsNotifMessage), 408 decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding), 409 decodeString(notif.text, mIsHexInput, notif.textEncoding)); 410 return message; 411 } 412 413 // change this to configure dialog display (for verification) 414 static public String getDialogTitle(GpsNiNotification notif, Context context) 415 { 416 return getNotifTitle(notif, context); 417 } 418 419 // change this to configure dialog display (for verification) 420 static private String getDialogMessage(GpsNiNotification notif, Context context) 421 { 422 return getNotifMessage(notif, context); 423 } 424 425 } 426