| /* |
| * Copyright (C) 2008 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.internal.location; |
| |
| import java.io.UnsupportedEncodingException; |
| |
| import android.app.Notification; |
| import android.app.NotificationManager; |
| import android.app.PendingIntent; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.location.LocationManager; |
| import android.location.INetInitiatedListener; |
| import android.telephony.TelephonyManager; |
| import android.telephony.PhoneNumberUtils; |
| import android.telephony.PhoneStateListener; |
| import android.os.Bundle; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.os.SystemProperties; |
| import android.provider.Settings; |
| import android.util.Log; |
| |
| import com.android.internal.R; |
| import com.android.internal.telephony.GsmAlphabet; |
| import com.android.internal.telephony.TelephonyProperties; |
| |
| /** |
| * A GPS Network-initiated Handler class used by LocationManager. |
| * |
| * {@hide} |
| */ |
| public class GpsNetInitiatedHandler { |
| |
| private static final String TAG = "GpsNetInitiatedHandler"; |
| |
| private static final boolean DEBUG = true; |
| private static final boolean VERBOSE = false; |
| |
| // NI verify activity for bringing up UI (not used yet) |
| public static final String ACTION_NI_VERIFY = "android.intent.action.NETWORK_INITIATED_VERIFY"; |
| |
| // string constants for defining data fields in NI Intent |
| public static final String NI_INTENT_KEY_NOTIF_ID = "notif_id"; |
| public static final String NI_INTENT_KEY_TITLE = "title"; |
| public static final String NI_INTENT_KEY_MESSAGE = "message"; |
| public static final String NI_INTENT_KEY_TIMEOUT = "timeout"; |
| public static final String NI_INTENT_KEY_DEFAULT_RESPONSE = "default_resp"; |
| |
| // the extra command to send NI response to GpsLocationProvider |
| public static final String NI_RESPONSE_EXTRA_CMD = "send_ni_response"; |
| |
| // the extra command parameter names in the Bundle |
| public static final String NI_EXTRA_CMD_NOTIF_ID = "notif_id"; |
| public static final String NI_EXTRA_CMD_RESPONSE = "response"; |
| |
| // these need to match GpsNiType constants in gps_ni.h |
| public static final int GPS_NI_TYPE_VOICE = 1; |
| public static final int GPS_NI_TYPE_UMTS_SUPL = 2; |
| public static final int GPS_NI_TYPE_UMTS_CTRL_PLANE = 3; |
| public static final int GPS_NI_TYPE_EMERGENCY_SUPL = 4; |
| |
| // these need to match GpsUserResponseType constants in gps_ni.h |
| public static final int GPS_NI_RESPONSE_ACCEPT = 1; |
| public static final int GPS_NI_RESPONSE_DENY = 2; |
| public static final int GPS_NI_RESPONSE_NORESP = 3; |
| public static final int GPS_NI_RESPONSE_IGNORE = 4; |
| |
| // these need to match GpsNiNotifyFlags constants in gps_ni.h |
| public static final int GPS_NI_NEED_NOTIFY = 0x0001; |
| public static final int GPS_NI_NEED_VERIFY = 0x0002; |
| public static final int GPS_NI_PRIVACY_OVERRIDE = 0x0004; |
| |
| // these need to match GpsNiEncodingType in gps_ni.h |
| public static final int GPS_ENC_NONE = 0; |
| public static final int GPS_ENC_SUPL_GSM_DEFAULT = 1; |
| public static final int GPS_ENC_SUPL_UTF8 = 2; |
| public static final int GPS_ENC_SUPL_UCS2 = 3; |
| public static final int GPS_ENC_UNKNOWN = -1; |
| |
| private final Context mContext; |
| private final TelephonyManager mTelephonyManager; |
| private final PhoneStateListener mPhoneStateListener; |
| |
| // parent gps location provider |
| private final LocationManager mLocationManager; |
| |
| // configuration of notificaiton behavior |
| private boolean mPlaySounds = false; |
| private boolean mPopupImmediately = true; |
| |
| // read the SUPL_ES form gps.conf |
| private volatile boolean mIsSuplEsEnabled; |
| |
| // Set to true if the phone is having emergency call. |
| private volatile boolean mIsInEmergency; |
| |
| // If Location function is enabled. |
| private volatile boolean mIsLocationEnabled = false; |
| |
| private final INetInitiatedListener mNetInitiatedListener; |
| |
| // Set to true if string from HAL is encoded as Hex, e.g., "3F0039" |
| static private boolean mIsHexInput = true; |
| |
| public static class GpsNiNotification |
| { |
| public int notificationId; |
| public int niType; |
| public boolean needNotify; |
| public boolean needVerify; |
| public boolean privacyOverride; |
| public int timeout; |
| public int defaultResponse; |
| public String requestorId; |
| public String text; |
| public int requestorIdEncoding; |
| public int textEncoding; |
| public Bundle extras; |
| }; |
| |
| public static class GpsNiResponse { |
| /* User response, one of the values in GpsUserResponseType */ |
| int userResponse; |
| /* Optional extra data to pass with the user response */ |
| Bundle extras; |
| }; |
| |
| private final BroadcastReceiver mBroadcastReciever = new BroadcastReceiver() { |
| |
| @Override public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| if (action.equals(Intent.ACTION_NEW_OUTGOING_CALL)) { |
| String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); |
| /* |
| Emergency Mode is when during emergency call or in emergency call back mode. |
| For checking if it is during emergency call: |
| mIsInEmergency records if the phone is in emergency call or not. It will |
| be set to true when the phone is having emergency call, and then will |
| be set to false by mPhoneStateListener when the emergency call ends. |
| For checking if it is in emergency call back mode: |
| Emergency call back mode will be checked by reading system properties |
| when necessary: SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE) |
| */ |
| setInEmergency(PhoneNumberUtils.isEmergencyNumber(phoneNumber)); |
| if (DEBUG) Log.v(TAG, "ACTION_NEW_OUTGOING_CALL - " + getInEmergency()); |
| } else if (action.equals(LocationManager.MODE_CHANGED_ACTION)) { |
| updateLocationMode(); |
| if (DEBUG) Log.d(TAG, "location enabled :" + getLocationEnabled()); |
| } |
| } |
| }; |
| |
| /** |
| * The notification that is shown when a network-initiated notification |
| * (and verification) event is received. |
| * <p> |
| * This is lazily created, so use {@link #setNINotification()}. |
| */ |
| private Notification mNiNotification; |
| |
| public GpsNetInitiatedHandler(Context context, |
| INetInitiatedListener netInitiatedListener, |
| boolean isSuplEsEnabled) { |
| mContext = context; |
| |
| if (netInitiatedListener == null) { |
| throw new IllegalArgumentException("netInitiatedListener is null"); |
| } else { |
| mNetInitiatedListener = netInitiatedListener; |
| } |
| |
| setSuplEsEnabled(isSuplEsEnabled); |
| mLocationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE); |
| updateLocationMode(); |
| mTelephonyManager = |
| (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); |
| |
| mPhoneStateListener = new PhoneStateListener() { |
| @Override |
| public void onCallStateChanged(int state, String incomingNumber) { |
| if (DEBUG) Log.d(TAG, "onCallStateChanged(): state is "+ state); |
| // listening for emergency call ends |
| if (state == TelephonyManager.CALL_STATE_IDLE) { |
| setInEmergency(false); |
| } |
| } |
| }; |
| mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); |
| |
| IntentFilter intentFilter = new IntentFilter(); |
| intentFilter.addAction(Intent.ACTION_NEW_OUTGOING_CALL); |
| intentFilter.addAction(LocationManager.MODE_CHANGED_ACTION); |
| mContext.registerReceiver(mBroadcastReciever, intentFilter); |
| } |
| |
| public void setSuplEsEnabled(boolean isEnabled) { |
| mIsSuplEsEnabled = isEnabled; |
| } |
| |
| public boolean getSuplEsEnabled() { |
| return mIsSuplEsEnabled; |
| } |
| |
| /** |
| * Updates Location enabler based on location setting. |
| */ |
| public void updateLocationMode() { |
| mIsLocationEnabled = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); |
| } |
| |
| /** |
| * Checks if user agreed to use location. |
| */ |
| public boolean getLocationEnabled() { |
| return mIsLocationEnabled; |
| } |
| |
| // Note: Currently, there are two mechanisms involved to determine if a |
| // phone is in emergency mode: |
| // 1. If the user is making an emergency call, this is provided by activly |
| // monitoring the outgoing phone number; |
| // 2. If the device is in a emergency callback state, this is provided by |
| // system properties. |
| // If either one of above exists, the phone is considered in an emergency |
| // mode. Because of this complexity, we need to be careful about how to set |
| // and clear the emergency state. |
| public void setInEmergency(boolean isInEmergency) { |
| mIsInEmergency = isInEmergency; |
| } |
| |
| public boolean getInEmergency() { |
| boolean isInEmergencyCallback = Boolean.parseBoolean( |
| SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE)); |
| return mIsInEmergency || isInEmergencyCallback; |
| } |
| |
| |
| // Handles NI events from HAL |
| public void handleNiNotification(GpsNiNotification notif) { |
| if (DEBUG) Log.d(TAG, "in handleNiNotification () :" |
| + " notificationId: " + notif.notificationId |
| + " requestorId: " + notif.requestorId |
| + " text: " + notif.text |
| + " mIsSuplEsEnabled" + getSuplEsEnabled() |
| + " mIsLocationEnabled" + getLocationEnabled()); |
| |
| if (getSuplEsEnabled()) { |
| handleNiInEs(notif); |
| } else { |
| handleNi(notif); |
| } |
| |
| ////////////////////////////////////////////////////////////////////////// |
| // A note about timeout |
| // According to the protocol, in the need_notify and need_verify case, |
| // a default response should be sent when time out. |
| // |
| // In some GPS hardware, the GPS driver (under HAL) can handle the timeout case |
| // and this class GpsNetInitiatedHandler does not need to do anything. |
| // |
| // However, the UI should at least close the dialog when timeout. Further, |
| // for more general handling, timeout response should be added to the Handler here. |
| // |
| } |
| |
| // handle NI form HAL when SUPL_ES is disabled. |
| private void handleNi(GpsNiNotification notif) { |
| if (DEBUG) Log.d(TAG, "in handleNi () :" |
| + " needNotify: " + notif.needNotify |
| + " needVerify: " + notif.needVerify |
| + " privacyOverride: " + notif.privacyOverride |
| + " mPopupImmediately: " + mPopupImmediately |
| + " mInEmergency: " + getInEmergency()); |
| |
| if (!getLocationEnabled() && !getInEmergency()) { |
| // Location is currently disabled, ignore all NI requests. |
| try { |
| mNetInitiatedListener.sendNiResponse(notif.notificationId, |
| GPS_NI_RESPONSE_IGNORE); |
| } catch (RemoteException e) { |
| Log.e(TAG, "RemoteException in sendNiResponse"); |
| } |
| } |
| if (notif.needNotify) { |
| // If NI does not need verify or the dialog is not requested |
| // to pop up immediately, the dialog box will not pop up. |
| if (notif.needVerify && mPopupImmediately) { |
| // Popup the dialog box now |
| openNiDialog(notif); |
| } else { |
| // Show the notification |
| setNiNotification(notif); |
| } |
| } |
| // ACCEPT cases: 1. Notify, no verify; 2. no notify, no verify; |
| // 3. privacy override. |
| if (!notif.needVerify || notif.privacyOverride) { |
| try { |
| mNetInitiatedListener.sendNiResponse(notif.notificationId, |
| GPS_NI_RESPONSE_ACCEPT); |
| } catch (RemoteException e) { |
| Log.e(TAG, "RemoteException in sendNiResponse"); |
| } |
| } |
| } |
| |
| // handle NI from HAL when the SUPL_ES is enabled |
| private void handleNiInEs(GpsNiNotification notif) { |
| |
| if (DEBUG) Log.d(TAG, "in handleNiInEs () :" |
| + " niType: " + notif.niType |
| + " notificationId: " + notif.notificationId); |
| |
| // UE is in emergency mode when in emergency call mode or in emergency call back mode |
| /* |
| 1. When SUPL ES bit is off and UE is not in emergency mode: |
| Call handleNi() to do legacy behaviour. |
| 2. When SUPL ES bit is on and UE is in emergency mode: |
| Call handleNi() to do acceptance behaviour. |
| 3. When SUPL ES bit is off but UE is in emergency mode: |
| Ignore the emergency SUPL INIT. |
| 4. When SUPL ES bit is on but UE is not in emergency mode: |
| Ignore the emergency SUPL INIT. |
| */ |
| boolean isNiTypeES = (notif.niType == GPS_NI_TYPE_EMERGENCY_SUPL); |
| if (isNiTypeES != getInEmergency()) { |
| try { |
| mNetInitiatedListener.sendNiResponse(notif.notificationId, |
| GPS_NI_RESPONSE_IGNORE); |
| } catch (RemoteException e) { |
| Log.e(TAG, "RemoteException in sendNiResponse"); |
| } |
| } else { |
| handleNi(notif); |
| } |
| } |
| |
| // Sets the NI notification. |
| private synchronized void setNiNotification(GpsNiNotification notif) { |
| NotificationManager notificationManager = (NotificationManager) mContext |
| .getSystemService(Context.NOTIFICATION_SERVICE); |
| if (notificationManager == null) { |
| return; |
| } |
| |
| String title = getNotifTitle(notif, mContext); |
| String message = getNotifMessage(notif, mContext); |
| |
| if (DEBUG) Log.d(TAG, "setNiNotification, notifyId: " + notif.notificationId + |
| ", title: " + title + |
| ", message: " + message); |
| |
| // Construct Notification |
| if (mNiNotification == null) { |
| mNiNotification = new Notification(); |
| mNiNotification.icon = com.android.internal.R.drawable.stat_sys_gps_on; /* Change notification icon here */ |
| mNiNotification.when = 0; |
| } |
| |
| if (mPlaySounds) { |
| mNiNotification.defaults |= Notification.DEFAULT_SOUND; |
| } else { |
| mNiNotification.defaults &= ~Notification.DEFAULT_SOUND; |
| } |
| |
| mNiNotification.flags = Notification.FLAG_ONGOING_EVENT | Notification.FLAG_AUTO_CANCEL; |
| mNiNotification.tickerText = getNotifTicker(notif, mContext); |
| |
| // if not to popup dialog immediately, pending intent will open the dialog |
| Intent intent = !mPopupImmediately ? getDlgIntent(notif) : new Intent(); |
| PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0); |
| mNiNotification.color = mContext.getResources().getColor( |
| com.android.internal.R.color.system_notification_accent_color); |
| mNiNotification.setLatestEventInfo(mContext, title, message, pi); |
| |
| notificationManager.notifyAsUser(null, notif.notificationId, mNiNotification, |
| UserHandle.ALL); |
| } |
| |
| // Opens the notification dialog and waits for user input |
| private void openNiDialog(GpsNiNotification notif) |
| { |
| Intent intent = getDlgIntent(notif); |
| |
| if (DEBUG) Log.d(TAG, "openNiDialog, notifyId: " + notif.notificationId + |
| ", requestorId: " + notif.requestorId + |
| ", text: " + notif.text); |
| |
| mContext.startActivity(intent); |
| } |
| |
| // Construct the intent for bringing up the dialog activity, which shows the |
| // notification and takes user input |
| private Intent getDlgIntent(GpsNiNotification notif) |
| { |
| Intent intent = new Intent(); |
| String title = getDialogTitle(notif, mContext); |
| String message = getDialogMessage(notif, mContext); |
| |
| // directly bring up the NI activity |
| intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); |
| intent.setClass(mContext, com.android.internal.app.NetInitiatedActivity.class); |
| |
| // put data in the intent |
| intent.putExtra(NI_INTENT_KEY_NOTIF_ID, notif.notificationId); |
| intent.putExtra(NI_INTENT_KEY_TITLE, title); |
| intent.putExtra(NI_INTENT_KEY_MESSAGE, message); |
| intent.putExtra(NI_INTENT_KEY_TIMEOUT, notif.timeout); |
| intent.putExtra(NI_INTENT_KEY_DEFAULT_RESPONSE, notif.defaultResponse); |
| |
| if (DEBUG) Log.d(TAG, "generateIntent, title: " + title + ", message: " + message + |
| ", timeout: " + notif.timeout); |
| |
| return intent; |
| } |
| |
| // Converts a string (or Hex string) to a char array |
| static byte[] stringToByteArray(String original, boolean isHex) |
| { |
| int length = isHex ? original.length() / 2 : original.length(); |
| byte[] output = new byte[length]; |
| int i; |
| |
| if (isHex) |
| { |
| for (i = 0; i < length; i++) |
| { |
| output[i] = (byte) Integer.parseInt(original.substring(i*2, i*2+2), 16); |
| } |
| } |
| else { |
| for (i = 0; i < length; i++) |
| { |
| output[i] = (byte) original.charAt(i); |
| } |
| } |
| |
| return output; |
| } |
| |
| /** |
| * Unpacks an byte array containing 7-bit packed characters into a String. |
| * |
| * @param input a 7-bit packed char array |
| * @return the unpacked String |
| */ |
| static String decodeGSMPackedString(byte[] input) |
| { |
| final char PADDING_CHAR = 0x00; |
| int lengthBytes = input.length; |
| int lengthSeptets = (lengthBytes * 8) / 7; |
| String decoded; |
| |
| /* Special case where the last 7 bits in the last byte could hold a valid |
| * 7-bit character or a padding character. Drop the last 7-bit character |
| * if it is a padding character. |
| */ |
| if (lengthBytes % 7 == 0) { |
| if (lengthBytes > 0) { |
| if ((input[lengthBytes - 1] >> 1) == PADDING_CHAR) { |
| lengthSeptets = lengthSeptets - 1; |
| } |
| } |
| } |
| |
| decoded = GsmAlphabet.gsm7BitPackedToString(input, 0, lengthSeptets); |
| |
| // Return "" if decoding of GSM packed string fails |
| if (null == decoded) { |
| Log.e(TAG, "Decoding of GSM packed string failed"); |
| decoded = ""; |
| } |
| |
| return decoded; |
| } |
| |
| static String decodeUTF8String(byte[] input) |
| { |
| String decoded = ""; |
| try { |
| decoded = new String(input, "UTF-8"); |
| } |
| catch (UnsupportedEncodingException e) |
| { |
| throw new AssertionError(); |
| } |
| return decoded; |
| } |
| |
| static String decodeUCS2String(byte[] input) |
| { |
| String decoded = ""; |
| try { |
| decoded = new String(input, "UTF-16"); |
| } |
| catch (UnsupportedEncodingException e) |
| { |
| throw new AssertionError(); |
| } |
| return decoded; |
| } |
| |
| /** Decode NI string |
| * |
| * @param original The text string to be decoded |
| * @param isHex Specifies whether the content of the string has been encoded as a Hex string. Encoding |
| * a string as Hex can allow zeros inside the coded text. |
| * @param coding Specifies the coding scheme of the string, such as GSM, UTF8, UCS2, etc. This coding scheme |
| * needs to match those used passed to HAL from the native GPS driver. Decoding is done according |
| * to the <code> coding </code>, after a Hex string is decoded. Generally, if the |
| * notification strings don't need further decoding, <code> coding </code> encoding can be |
| * set to -1, and <code> isHex </code> can be false. |
| * @return the decoded string |
| */ |
| static private String decodeString(String original, boolean isHex, int coding) |
| { |
| String decoded = original; |
| byte[] input = stringToByteArray(original, isHex); |
| |
| switch (coding) { |
| case GPS_ENC_NONE: |
| decoded = original; |
| break; |
| |
| case GPS_ENC_SUPL_GSM_DEFAULT: |
| decoded = decodeGSMPackedString(input); |
| break; |
| |
| case GPS_ENC_SUPL_UTF8: |
| decoded = decodeUTF8String(input); |
| break; |
| |
| case GPS_ENC_SUPL_UCS2: |
| decoded = decodeUCS2String(input); |
| break; |
| |
| case GPS_ENC_UNKNOWN: |
| decoded = original; |
| break; |
| |
| default: |
| Log.e(TAG, "Unknown encoding " + coding + " for NI text " + original); |
| break; |
| } |
| return decoded; |
| } |
| |
| // change this to configure notification display |
| static private String getNotifTicker(GpsNiNotification notif, Context context) |
| { |
| String ticker = String.format(context.getString(R.string.gpsNotifTicker), |
| decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding), |
| decodeString(notif.text, mIsHexInput, notif.textEncoding)); |
| return ticker; |
| } |
| |
| // change this to configure notification display |
| static private String getNotifTitle(GpsNiNotification notif, Context context) |
| { |
| String title = String.format(context.getString(R.string.gpsNotifTitle)); |
| return title; |
| } |
| |
| // change this to configure notification display |
| static private String getNotifMessage(GpsNiNotification notif, Context context) |
| { |
| String message = String.format(context.getString(R.string.gpsNotifMessage), |
| decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding), |
| decodeString(notif.text, mIsHexInput, notif.textEncoding)); |
| return message; |
| } |
| |
| // change this to configure dialog display (for verification) |
| static public String getDialogTitle(GpsNiNotification notif, Context context) |
| { |
| return getNotifTitle(notif, context); |
| } |
| |
| // change this to configure dialog display (for verification) |
| static private String getDialogMessage(GpsNiNotification notif, Context context) |
| { |
| return getNotifMessage(notif, context); |
| } |
| |
| } |