2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.internal.location;
19 import java.io.UnsupportedEncodingException;
20 import java.util.concurrent.TimeUnit;
22 import android.app.Notification;
23 import android.app.NotificationManager;
24 import android.app.PendingIntent;
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.location.LocationManager;
30 import android.location.INetInitiatedListener;
31 import android.os.SystemClock;
32 import android.telephony.TelephonyManager;
33 import android.telephony.PhoneNumberUtils;
34 import android.telephony.PhoneStateListener;
35 import android.os.Bundle;
36 import android.os.RemoteException;
37 import android.os.UserHandle;
38 import android.os.SystemProperties;
39 import android.util.Log;
41 import com.android.internal.R;
42 import com.android.internal.telephony.GsmAlphabet;
43 import com.android.internal.telephony.TelephonyProperties;
46 * A GPS Network-initiated Handler class used by LocationManager.
50 public class GpsNetInitiatedHandler {
52 private static final String TAG = "GpsNetInitiatedHandler";
54 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
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";
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";
66 // the extra command to send NI response to GnssLocationProvider
67 public static final String NI_RESPONSE_EXTRA_CMD = "send_ni_response";
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";
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;
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;
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;
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;
97 // Limit on SUPL NI emergency mode time extension after emergency sessions ends
98 private static final int MAX_EMERGENCY_MODE_EXTENSION_SECONDS = 300; // 5 minute maximum
100 private final Context mContext;
101 private final TelephonyManager mTelephonyManager;
102 private final PhoneStateListener mPhoneStateListener;
104 // parent gps location provider
105 private final LocationManager mLocationManager;
107 // configuration of notificaiton behavior
108 private boolean mPlaySounds = false;
109 private boolean mPopupImmediately = true;
111 // read the SUPL_ES form gps.conf
112 private volatile boolean mIsSuplEsEnabled;
114 // Set to true if the phone is having emergency call.
115 private volatile boolean mIsInEmergencyCall;
117 // If Location function is enabled.
118 private volatile boolean mIsLocationEnabled = false;
120 private final INetInitiatedListener mNetInitiatedListener;
122 // Set to true if string from HAL is encoded as Hex, e.g., "3F0039"
123 static private boolean mIsHexInput = true;
125 // End time of emergency call, and extension, if set
126 private long mCallEndElapsedRealtimeMillis = 0;
127 private long mEmergencyExtensionMillis = 0;
129 public static class GpsNiNotification
131 public int notificationId;
133 public boolean needNotify;
134 public boolean needVerify;
135 public boolean privacyOverride;
137 public int defaultResponse;
138 public String requestorId;
140 public int requestorIdEncoding;
141 public int textEncoding;
142 public Bundle extras;
145 public static class GpsNiResponse {
146 /* User response, one of the values in GpsUserResponseType */
148 /* Optional extra data to pass with the user response */
152 private final BroadcastReceiver mBroadcastReciever = new BroadcastReceiver() {
154 @Override public void onReceive(Context context, Intent intent) {
155 String action = intent.getAction();
156 if (action.equals(Intent.ACTION_NEW_OUTGOING_CALL)) {
157 String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
159 Tracks the emergency call:
160 mIsInEmergencyCall records if the phone is in emergency call or not. It will
161 be set to true when the phone is having emergency call, and then will
162 be set to false by mPhoneStateListener when the emergency call ends.
164 mIsInEmergencyCall = PhoneNumberUtils.isEmergencyNumber(phoneNumber);
165 if (DEBUG) Log.v(TAG, "ACTION_NEW_OUTGOING_CALL - " + getInEmergency());
166 } else if (action.equals(LocationManager.MODE_CHANGED_ACTION)) {
167 updateLocationMode();
168 if (DEBUG) Log.d(TAG, "location enabled :" + getLocationEnabled());
174 * The notification that is shown when a network-initiated notification
175 * (and verification) event is received.
177 * This is lazily created, so use {@link #setNINotification()}.
179 private Notification.Builder mNiNotificationBuilder;
181 public GpsNetInitiatedHandler(Context context,
182 INetInitiatedListener netInitiatedListener,
183 boolean isSuplEsEnabled) {
186 if (netInitiatedListener == null) {
187 throw new IllegalArgumentException("netInitiatedListener is null");
189 mNetInitiatedListener = netInitiatedListener;
192 setSuplEsEnabled(isSuplEsEnabled);
193 mLocationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
194 updateLocationMode();
196 (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
198 mPhoneStateListener = new PhoneStateListener() {
200 public void onCallStateChanged(int state, String incomingNumber) {
201 if (DEBUG) Log.d(TAG, "onCallStateChanged(): state is "+ state);
202 // listening for emergency call ends
203 if (state == TelephonyManager.CALL_STATE_IDLE) {
204 if (mIsInEmergencyCall) {
205 mCallEndElapsedRealtimeMillis = SystemClock.elapsedRealtime();
206 mIsInEmergencyCall = false;
211 mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
213 IntentFilter intentFilter = new IntentFilter();
214 intentFilter.addAction(Intent.ACTION_NEW_OUTGOING_CALL);
215 intentFilter.addAction(LocationManager.MODE_CHANGED_ACTION);
216 mContext.registerReceiver(mBroadcastReciever, intentFilter);
219 public void setSuplEsEnabled(boolean isEnabled) {
220 mIsSuplEsEnabled = isEnabled;
223 public boolean getSuplEsEnabled() {
224 return mIsSuplEsEnabled;
228 * Updates Location enabler based on location setting.
230 public void updateLocationMode() {
231 mIsLocationEnabled = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
235 * Checks if user agreed to use location.
237 public boolean getLocationEnabled() {
238 return mIsLocationEnabled;
242 * Determines whether device is in user-initiated emergency session based on the following
243 * 1. If the user is making an emergency call, this is provided by actively
244 * monitoring the outgoing phone number;
245 * 2. If the user has recently ended an emergency call, and the device is in a configured time
246 * window after the end of that call.
247 * 3. If the device is in a emergency callback state, this is provided by querying
249 * @return true if is considered in user initiated emergency mode for NI purposes
251 public boolean getInEmergency() {
252 boolean isInEmergencyExtension =
253 (SystemClock.elapsedRealtime() - mCallEndElapsedRealtimeMillis) <
254 mEmergencyExtensionMillis;
255 boolean isInEmergencyCallback = Boolean.parseBoolean(
256 SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE));
257 return mIsInEmergencyCall || isInEmergencyCallback || isInEmergencyExtension;
260 public void setEmergencyExtensionSeconds(int emergencyExtensionSeconds) {
261 if (emergencyExtensionSeconds > MAX_EMERGENCY_MODE_EXTENSION_SECONDS) {
262 Log.w(TAG, "emergencyExtensionSeconds " + emergencyExtensionSeconds
263 + " too high, reset to " + MAX_EMERGENCY_MODE_EXTENSION_SECONDS);
264 emergencyExtensionSeconds = MAX_EMERGENCY_MODE_EXTENSION_SECONDS;
265 } else if (emergencyExtensionSeconds < 0) {
266 Log.w(TAG, "emergencyExtensionSeconds " + emergencyExtensionSeconds
267 + " is negative, reset to zero.");
268 emergencyExtensionSeconds = 0;
270 mEmergencyExtensionMillis = TimeUnit.SECONDS.toMillis(emergencyExtensionSeconds);
274 // Handles NI events from HAL
275 public void handleNiNotification(GpsNiNotification notif) {
276 if (DEBUG) Log.d(TAG, "in handleNiNotification () :"
277 + " notificationId: " + notif.notificationId
278 + " requestorId: " + notif.requestorId
279 + " text: " + notif.text
280 + " mIsSuplEsEnabled" + getSuplEsEnabled()
281 + " mIsLocationEnabled" + getLocationEnabled());
283 if (getSuplEsEnabled()) {
289 //////////////////////////////////////////////////////////////////////////
290 // A note about timeout
291 // According to the protocol, in the need_notify and need_verify case,
292 // a default response should be sent when time out.
294 // In some GPS hardware, the GPS driver (under HAL) can handle the timeout case
295 // and this class GpsNetInitiatedHandler does not need to do anything.
297 // However, the UI should at least close the dialog when timeout. Further,
298 // for more general handling, timeout response should be added to the Handler here.
302 // handle NI form HAL when SUPL_ES is disabled.
303 private void handleNi(GpsNiNotification notif) {
304 if (DEBUG) Log.d(TAG, "in handleNi () :"
305 + " needNotify: " + notif.needNotify
306 + " needVerify: " + notif.needVerify
307 + " privacyOverride: " + notif.privacyOverride
308 + " mPopupImmediately: " + mPopupImmediately
309 + " mInEmergency: " + getInEmergency());
311 if (!getLocationEnabled() && !getInEmergency()) {
312 // Location is currently disabled, ignore all NI requests.
314 mNetInitiatedListener.sendNiResponse(notif.notificationId,
315 GPS_NI_RESPONSE_IGNORE);
316 } catch (RemoteException e) {
317 Log.e(TAG, "RemoteException in sendNiResponse");
320 if (notif.needNotify) {
321 // If NI does not need verify or the dialog is not requested
322 // to pop up immediately, the dialog box will not pop up.
323 if (notif.needVerify && mPopupImmediately) {
324 // Popup the dialog box now
327 // Show the notification
328 setNiNotification(notif);
331 // ACCEPT cases: 1. Notify, no verify; 2. no notify, no verify;
332 // 3. privacy override.
333 if (!notif.needVerify || notif.privacyOverride) {
335 mNetInitiatedListener.sendNiResponse(notif.notificationId,
336 GPS_NI_RESPONSE_ACCEPT);
337 } catch (RemoteException e) {
338 Log.e(TAG, "RemoteException in sendNiResponse");
343 // handle NI from HAL when the SUPL_ES is enabled
344 private void handleNiInEs(GpsNiNotification notif) {
346 if (DEBUG) Log.d(TAG, "in handleNiInEs () :"
347 + " niType: " + notif.niType
348 + " notificationId: " + notif.notificationId);
350 // UE is in emergency mode when in emergency call mode or in emergency call back mode
352 1. When SUPL ES bit is off and UE is not in emergency mode:
353 Call handleNi() to do legacy behaviour.
354 2. When SUPL ES bit is on and UE is in emergency mode:
355 Call handleNi() to do acceptance behaviour.
356 3. When SUPL ES bit is off but UE is in emergency mode:
357 Ignore the emergency SUPL INIT.
358 4. When SUPL ES bit is on but UE is not in emergency mode:
359 Ignore the emergency SUPL INIT.
361 boolean isNiTypeES = (notif.niType == GPS_NI_TYPE_EMERGENCY_SUPL);
362 if (isNiTypeES != getInEmergency()) {
364 mNetInitiatedListener.sendNiResponse(notif.notificationId,
365 GPS_NI_RESPONSE_IGNORE);
366 } catch (RemoteException e) {
367 Log.e(TAG, "RemoteException in sendNiResponse");
374 // Sets the NI notification.
375 private synchronized void setNiNotification(GpsNiNotification notif) {
376 NotificationManager notificationManager = (NotificationManager) mContext
377 .getSystemService(Context.NOTIFICATION_SERVICE);
378 if (notificationManager == null) {
382 String title = getNotifTitle(notif, mContext);
383 String message = getNotifMessage(notif, mContext);
385 if (DEBUG) Log.d(TAG, "setNiNotification, notifyId: " + notif.notificationId +
386 ", title: " + title +
387 ", message: " + message);
389 // Construct Notification
390 if (mNiNotificationBuilder == null) {
391 mNiNotificationBuilder = new Notification.Builder(mContext)
392 .setSmallIcon(com.android.internal.R.drawable.stat_sys_gps_on)
396 .setColor(mContext.getColor(
397 com.android.internal.R.color.system_notification_accent_color));
401 mNiNotificationBuilder.setDefaults(Notification.DEFAULT_SOUND);
403 mNiNotificationBuilder.setDefaults(0);
406 // if not to popup dialog immediately, pending intent will open the dialog
407 Intent intent = !mPopupImmediately ? getDlgIntent(notif) : new Intent();
408 PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0);
409 mNiNotificationBuilder.setTicker(getNotifTicker(notif, mContext))
410 .setContentTitle(title)
411 .setContentText(message)
412 .setContentIntent(pi);
414 notificationManager.notifyAsUser(null, notif.notificationId, mNiNotificationBuilder.build(),
418 // Opens the notification dialog and waits for user input
419 private void openNiDialog(GpsNiNotification notif)
421 Intent intent = getDlgIntent(notif);
423 if (DEBUG) Log.d(TAG, "openNiDialog, notifyId: " + notif.notificationId +
424 ", requestorId: " + notif.requestorId +
425 ", text: " + notif.text);
427 mContext.startActivity(intent);
430 // Construct the intent for bringing up the dialog activity, which shows the
431 // notification and takes user input
432 private Intent getDlgIntent(GpsNiNotification notif)
434 Intent intent = new Intent();
435 String title = getDialogTitle(notif, mContext);
436 String message = getDialogMessage(notif, mContext);
438 // directly bring up the NI activity
439 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
440 intent.setClass(mContext, com.android.internal.app.NetInitiatedActivity.class);
442 // put data in the intent
443 intent.putExtra(NI_INTENT_KEY_NOTIF_ID, notif.notificationId);
444 intent.putExtra(NI_INTENT_KEY_TITLE, title);
445 intent.putExtra(NI_INTENT_KEY_MESSAGE, message);
446 intent.putExtra(NI_INTENT_KEY_TIMEOUT, notif.timeout);
447 intent.putExtra(NI_INTENT_KEY_DEFAULT_RESPONSE, notif.defaultResponse);
449 if (DEBUG) Log.d(TAG, "generateIntent, title: " + title + ", message: " + message +
450 ", timeout: " + notif.timeout);
455 // Converts a string (or Hex string) to a char array
456 static byte[] stringToByteArray(String original, boolean isHex)
458 int length = isHex ? original.length() / 2 : original.length();
459 byte[] output = new byte[length];
464 for (i = 0; i < length; i++)
466 output[i] = (byte) Integer.parseInt(original.substring(i*2, i*2+2), 16);
470 for (i = 0; i < length; i++)
472 output[i] = (byte) original.charAt(i);
480 * Unpacks an byte array containing 7-bit packed characters into a String.
482 * @param input a 7-bit packed char array
483 * @return the unpacked String
485 static String decodeGSMPackedString(byte[] input)
487 final char PADDING_CHAR = 0x00;
488 int lengthBytes = input.length;
489 int lengthSeptets = (lengthBytes * 8) / 7;
492 /* Special case where the last 7 bits in the last byte could hold a valid
493 * 7-bit character or a padding character. Drop the last 7-bit character
494 * if it is a padding character.
496 if (lengthBytes % 7 == 0) {
497 if (lengthBytes > 0) {
498 if ((input[lengthBytes - 1] >> 1) == PADDING_CHAR) {
499 lengthSeptets = lengthSeptets - 1;
504 decoded = GsmAlphabet.gsm7BitPackedToString(input, 0, lengthSeptets);
506 // Return "" if decoding of GSM packed string fails
507 if (null == decoded) {
508 Log.e(TAG, "Decoding of GSM packed string failed");
515 static String decodeUTF8String(byte[] input)
519 decoded = new String(input, "UTF-8");
521 catch (UnsupportedEncodingException e)
523 throw new AssertionError();
528 static String decodeUCS2String(byte[] input)
532 decoded = new String(input, "UTF-16");
534 catch (UnsupportedEncodingException e)
536 throw new AssertionError();
543 * @param original The text string to be decoded
544 * @param isHex Specifies whether the content of the string has been encoded as a Hex string. Encoding
545 * a string as Hex can allow zeros inside the coded text.
546 * @param coding Specifies the coding scheme of the string, such as GSM, UTF8, UCS2, etc. This coding scheme
547 * needs to match those used passed to HAL from the native GPS driver. Decoding is done according
548 * to the <code> coding </code>, after a Hex string is decoded. Generally, if the
549 * notification strings don't need further decoding, <code> coding </code> encoding can be
550 * set to -1, and <code> isHex </code> can be false.
551 * @return the decoded string
553 static private String decodeString(String original, boolean isHex, int coding)
555 String decoded = original;
556 byte[] input = stringToByteArray(original, isHex);
563 case GPS_ENC_SUPL_GSM_DEFAULT:
564 decoded = decodeGSMPackedString(input);
567 case GPS_ENC_SUPL_UTF8:
568 decoded = decodeUTF8String(input);
571 case GPS_ENC_SUPL_UCS2:
572 decoded = decodeUCS2String(input);
575 case GPS_ENC_UNKNOWN:
580 Log.e(TAG, "Unknown encoding " + coding + " for NI text " + original);
586 // change this to configure notification display
587 static private String getNotifTicker(GpsNiNotification notif, Context context)
589 String ticker = String.format(context.getString(R.string.gpsNotifTicker),
590 decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding),
591 decodeString(notif.text, mIsHexInput, notif.textEncoding));
595 // change this to configure notification display
596 static private String getNotifTitle(GpsNiNotification notif, Context context)
598 String title = String.format(context.getString(R.string.gpsNotifTitle));
602 // change this to configure notification display
603 static private String getNotifMessage(GpsNiNotification notif, Context context)
605 String message = String.format(context.getString(R.string.gpsNotifMessage),
606 decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding),
607 decodeString(notif.text, mIsHexInput, notif.textEncoding));
611 // change this to configure dialog display (for verification)
612 static public String getDialogTitle(GpsNiNotification notif, Context context)
614 return getNotifTitle(notif, context);
617 // change this to configure dialog display (for verification)
618 static private String getDialogMessage(GpsNiNotification notif, Context context)
620 return getNotifMessage(notif, context);