Home | History | Annotate | Download | only in location
      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.BroadcastReceiver;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.IntentFilter;
     28 import android.location.LocationManager;
     29 import android.location.INetInitiatedListener;
     30 import android.telephony.TelephonyManager;
     31 import android.telephony.PhoneNumberUtils;
     32 import android.telephony.PhoneStateListener;
     33 import android.os.Bundle;
     34 import android.os.RemoteException;
     35 import android.os.UserHandle;
     36 import android.os.SystemProperties;
     37 import android.provider.Settings;
     38 import android.util.Log;
     39 
     40 import com.android.internal.R;
     41 import com.android.internal.telephony.GsmAlphabet;
     42 import com.android.internal.telephony.TelephonyProperties;
     43 
     44 /**
     45  * A GPS Network-initiated Handler class used by LocationManager.
     46  *
     47  * {@hide}
     48  */
     49 public class GpsNetInitiatedHandler {
     50 
     51     private static final String TAG = "GpsNetInitiatedHandler";
     52 
     53     private static final boolean DEBUG = true;
     54     private static final boolean VERBOSE = false;
     55 
     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";
     58 
     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";
     65 
     66     // the extra command to send NI response to GpsLocationProvider
     67     public static final String NI_RESPONSE_EXTRA_CMD = "send_ni_response";
     68 
     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";
     72 
     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;
     78 
     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;
     84 
     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;
     89 
     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;
     96 
     97     private final Context mContext;
     98     private final TelephonyManager mTelephonyManager;
     99     private final PhoneStateListener mPhoneStateListener;
    100 
    101     // parent gps location provider
    102     private final LocationManager mLocationManager;
    103 
    104     // configuration of notificaiton behavior
    105     private boolean mPlaySounds = false;
    106     private boolean mPopupImmediately = true;
    107 
    108     // read the SUPL_ES form gps.conf
    109     private volatile boolean mIsSuplEsEnabled;
    110 
    111     // Set to true if the phone is having emergency call.
    112     private volatile boolean mIsInEmergency;
    113 
    114     // If Location function is enabled.
    115     private volatile boolean mIsLocationEnabled = false;
    116 
    117     private final INetInitiatedListener mNetInitiatedListener;
    118 
    119     // Set to true if string from HAL is encoded as Hex, e.g., "3F0039"
    120     static private boolean mIsHexInput = true;
    121 
    122     public static class GpsNiNotification
    123     {
    124         public int notificationId;
    125         public int niType;
    126         public boolean needNotify;
    127         public boolean needVerify;
    128         public boolean privacyOverride;
    129         public int timeout;
    130         public int defaultResponse;
    131         public String requestorId;
    132         public String text;
    133         public int requestorIdEncoding;
    134         public int textEncoding;
    135         public Bundle extras;
    136     };
    137 
    138     public static class GpsNiResponse {
    139         /* User response, one of the values in GpsUserResponseType */
    140         int userResponse;
    141         /* Optional extra data to pass with the user response */
    142         Bundle extras;
    143     };
    144 
    145     private final BroadcastReceiver mBroadcastReciever = new BroadcastReceiver() {
    146 
    147         @Override public void onReceive(Context context, Intent intent) {
    148             String action = intent.getAction();
    149             if (action.equals(Intent.ACTION_NEW_OUTGOING_CALL)) {
    150                 String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
    151                 /*
    152                    Emergency Mode is when during emergency call or in emergency call back mode.
    153                    For checking if it is during emergency call:
    154                        mIsInEmergency records if the phone is in emergency call or not. It will
    155                        be set to true when the phone is having emergency call, and then will
    156                        be set to false by mPhoneStateListener when the emergency call ends.
    157                    For checking if it is in emergency call back mode:
    158                        Emergency call back mode will be checked by reading system properties
    159                        when necessary: SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE)
    160                 */
    161                 setInEmergency(PhoneNumberUtils.isEmergencyNumber(phoneNumber));
    162                 if (DEBUG) Log.v(TAG, "ACTION_NEW_OUTGOING_CALL - " + getInEmergency());
    163             } else if (action.equals(LocationManager.MODE_CHANGED_ACTION)) {
    164                 updateLocationMode();
    165                 if (DEBUG) Log.d(TAG, "location enabled :" + getLocationEnabled());
    166             }
    167         }
    168     };
    169 
    170     /**
    171      * The notification that is shown when a network-initiated notification
    172      * (and verification) event is received.
    173      * <p>
    174      * This is lazily created, so use {@link #setNINotification()}.
    175      */
    176     private Notification mNiNotification;
    177 
    178     public GpsNetInitiatedHandler(Context context,
    179                                   INetInitiatedListener netInitiatedListener,
    180                                   boolean isSuplEsEnabled) {
    181         mContext = context;
    182 
    183         if (netInitiatedListener == null) {
    184             throw new IllegalArgumentException("netInitiatedListener is null");
    185         } else {
    186             mNetInitiatedListener = netInitiatedListener;
    187         }
    188 
    189         setSuplEsEnabled(isSuplEsEnabled);
    190         mLocationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
    191         updateLocationMode();
    192         mTelephonyManager =
    193             (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
    194 
    195         mPhoneStateListener = new PhoneStateListener() {
    196             @Override
    197             public void onCallStateChanged(int state, String incomingNumber) {
    198                 if (DEBUG) Log.d(TAG, "onCallStateChanged(): state is "+ state);
    199                 // listening for emergency call ends
    200                 if (state == TelephonyManager.CALL_STATE_IDLE) {
    201                     setInEmergency(false);
    202                 }
    203             }
    204         };
    205         mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
    206 
    207         IntentFilter intentFilter = new IntentFilter();
    208         intentFilter.addAction(Intent.ACTION_NEW_OUTGOING_CALL);
    209         intentFilter.addAction(LocationManager.MODE_CHANGED_ACTION);
    210         mContext.registerReceiver(mBroadcastReciever, intentFilter);
    211     }
    212 
    213     public void setSuplEsEnabled(boolean isEnabled) {
    214         mIsSuplEsEnabled = isEnabled;
    215     }
    216 
    217     public boolean getSuplEsEnabled() {
    218         return mIsSuplEsEnabled;
    219     }
    220 
    221     /**
    222      * Updates Location enabler based on location setting.
    223      */
    224     public void updateLocationMode() {
    225         mIsLocationEnabled = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
    226     }
    227 
    228     /**
    229      * Checks if user agreed to use location.
    230      */
    231     public boolean getLocationEnabled() {
    232         return mIsLocationEnabled;
    233     }
    234 
    235     // Note: Currently, there are two mechanisms involved to determine if a
    236     // phone is in emergency mode:
    237     // 1. If the user is making an emergency call, this is provided by activly
    238     //    monitoring the outgoing phone number;
    239     // 2. If the device is in a emergency callback state, this is provided by
    240     //    system properties.
    241     // If either one of above exists, the phone is considered in an emergency
    242     // mode. Because of this complexity, we need to be careful about how to set
    243     // and clear the emergency state.
    244     public void setInEmergency(boolean isInEmergency) {
    245         mIsInEmergency = isInEmergency;
    246     }
    247 
    248     public boolean getInEmergency() {
    249         boolean isInEmergencyCallback = Boolean.parseBoolean(
    250                 SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE));
    251         return mIsInEmergency || isInEmergencyCallback;
    252     }
    253 
    254 
    255     // Handles NI events from HAL
    256     public void handleNiNotification(GpsNiNotification notif) {
    257         if (DEBUG) Log.d(TAG, "in handleNiNotification () :"
    258                         + " notificationId: " + notif.notificationId
    259                         + " requestorId: " + notif.requestorId
    260                         + " text: " + notif.text
    261                         + " mIsSuplEsEnabled" + getSuplEsEnabled()
    262                         + " mIsLocationEnabled" + getLocationEnabled());
    263 
    264         if (getSuplEsEnabled()) {
    265             handleNiInEs(notif);
    266         } else {
    267             handleNi(notif);
    268         }
    269 
    270         //////////////////////////////////////////////////////////////////////////
    271         //   A note about timeout
    272         //   According to the protocol, in the need_notify and need_verify case,
    273         //   a default response should be sent when time out.
    274         //
    275         //   In some GPS hardware, the GPS driver (under HAL) can handle the timeout case
    276         //   and this class GpsNetInitiatedHandler does not need to do anything.
    277         //
    278         //   However, the UI should at least close the dialog when timeout. Further,
    279         //   for more general handling, timeout response should be added to the Handler here.
    280         //
    281     }
    282 
    283     // handle NI form HAL when SUPL_ES is disabled.
    284     private void handleNi(GpsNiNotification notif) {
    285         if (DEBUG) Log.d(TAG, "in handleNi () :"
    286                         + " needNotify: " + notif.needNotify
    287                         + " needVerify: " + notif.needVerify
    288                         + " privacyOverride: " + notif.privacyOverride
    289                         + " mPopupImmediately: " + mPopupImmediately
    290                         + " mInEmergency: " + getInEmergency());
    291 
    292         if (!getLocationEnabled() && !getInEmergency()) {
    293             // Location is currently disabled, ignore all NI requests.
    294             try {
    295                 mNetInitiatedListener.sendNiResponse(notif.notificationId,
    296                                                      GPS_NI_RESPONSE_IGNORE);
    297             } catch (RemoteException e) {
    298                 Log.e(TAG, "RemoteException in sendNiResponse");
    299             }
    300         }
    301         if (notif.needNotify) {
    302         // If NI does not need verify or the dialog is not requested
    303         // to pop up immediately, the dialog box will not pop up.
    304             if (notif.needVerify && mPopupImmediately) {
    305                 // Popup the dialog box now
    306                 openNiDialog(notif);
    307             } else {
    308                 // Show the notification
    309                 setNiNotification(notif);
    310             }
    311         }
    312         // ACCEPT cases: 1. Notify, no verify; 2. no notify, no verify;
    313         // 3. privacy override.
    314         if (!notif.needVerify || notif.privacyOverride) {
    315             try {
    316                 mNetInitiatedListener.sendNiResponse(notif.notificationId,
    317                                                      GPS_NI_RESPONSE_ACCEPT);
    318             } catch (RemoteException e) {
    319                 Log.e(TAG, "RemoteException in sendNiResponse");
    320             }
    321         }
    322     }
    323 
    324     // handle NI from HAL when the SUPL_ES is enabled
    325     private void handleNiInEs(GpsNiNotification notif) {
    326 
    327         if (DEBUG) Log.d(TAG, "in handleNiInEs () :"
    328                     + " niType: " + notif.niType
    329                     + " notificationId: " + notif.notificationId);
    330 
    331         // UE is in emergency mode when in emergency call mode or in emergency call back mode
    332         /*
    333            1. When SUPL ES bit is off and UE is not in emergency mode:
    334                   Call handleNi() to do legacy behaviour.
    335            2. When SUPL ES bit is on and UE is in emergency mode:
    336                   Call handleNi() to do acceptance behaviour.
    337            3. When SUPL ES bit is off but UE is in emergency mode:
    338                   Ignore the emergency SUPL INIT.
    339            4. When SUPL ES bit is on but UE is not in emergency mode:
    340                   Ignore the emergency SUPL INIT.
    341         */
    342         boolean isNiTypeES = (notif.niType == GPS_NI_TYPE_EMERGENCY_SUPL);
    343         if (isNiTypeES != getInEmergency()) {
    344             try {
    345                 mNetInitiatedListener.sendNiResponse(notif.notificationId,
    346                                                      GPS_NI_RESPONSE_IGNORE);
    347             } catch (RemoteException e) {
    348                 Log.e(TAG, "RemoteException in sendNiResponse");
    349             }
    350         } else {
    351             handleNi(notif);
    352         }
    353     }
    354 
    355     // Sets the NI notification.
    356     private synchronized void setNiNotification(GpsNiNotification notif) {
    357         NotificationManager notificationManager = (NotificationManager) mContext
    358                 .getSystemService(Context.NOTIFICATION_SERVICE);
    359         if (notificationManager == null) {
    360             return;
    361         }
    362 
    363         String title = getNotifTitle(notif, mContext);
    364         String message = getNotifMessage(notif, mContext);
    365 
    366         if (DEBUG) Log.d(TAG, "setNiNotification, notifyId: " + notif.notificationId +
    367                 ", title: " + title +
    368                 ", message: " + message);
    369 
    370         // Construct Notification
    371         if (mNiNotification == null) {
    372             mNiNotification = new Notification();
    373             mNiNotification.icon = com.android.internal.R.drawable.stat_sys_gps_on; /* Change notification icon here */
    374             mNiNotification.when = 0;
    375         }
    376 
    377         if (mPlaySounds) {
    378             mNiNotification.defaults |= Notification.DEFAULT_SOUND;
    379         } else {
    380             mNiNotification.defaults &= ~Notification.DEFAULT_SOUND;
    381         }
    382 
    383         mNiNotification.flags = Notification.FLAG_ONGOING_EVENT | Notification.FLAG_AUTO_CANCEL;
    384         mNiNotification.tickerText = getNotifTicker(notif, mContext);
    385 
    386         // if not to popup dialog immediately, pending intent will open the dialog
    387         Intent intent = !mPopupImmediately ? getDlgIntent(notif) : new Intent();
    388         PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0);
    389         mNiNotification.color = mContext.getResources().getColor(
    390                 com.android.internal.R.color.system_notification_accent_color);
    391         mNiNotification.setLatestEventInfo(mContext, title, message, pi);
    392 
    393         notificationManager.notifyAsUser(null, notif.notificationId, mNiNotification,
    394                 UserHandle.ALL);
    395     }
    396 
    397     // Opens the notification dialog and waits for user input
    398     private void openNiDialog(GpsNiNotification notif)
    399     {
    400         Intent intent = getDlgIntent(notif);
    401 
    402         if (DEBUG) Log.d(TAG, "openNiDialog, notifyId: " + notif.notificationId +
    403                 ", requestorId: " + notif.requestorId +
    404                 ", text: " + notif.text);
    405 
    406         mContext.startActivity(intent);
    407     }
    408 
    409     // Construct the intent for bringing up the dialog activity, which shows the
    410     // notification and takes user input
    411     private Intent getDlgIntent(GpsNiNotification notif)
    412     {
    413         Intent intent = new Intent();
    414         String title = getDialogTitle(notif, mContext);
    415         String message = getDialogMessage(notif, mContext);
    416 
    417         // directly bring up the NI activity
    418         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
    419         intent.setClass(mContext, com.android.internal.app.NetInitiatedActivity.class);
    420 
    421         // put data in the intent
    422         intent.putExtra(NI_INTENT_KEY_NOTIF_ID, notif.notificationId);
    423         intent.putExtra(NI_INTENT_KEY_TITLE, title);
    424         intent.putExtra(NI_INTENT_KEY_MESSAGE, message);
    425         intent.putExtra(NI_INTENT_KEY_TIMEOUT, notif.timeout);
    426         intent.putExtra(NI_INTENT_KEY_DEFAULT_RESPONSE, notif.defaultResponse);
    427 
    428         if (DEBUG) Log.d(TAG, "generateIntent, title: " + title + ", message: " + message +
    429                 ", timeout: " + notif.timeout);
    430 
    431         return intent;
    432     }
    433 
    434     // Converts a string (or Hex string) to a char array
    435     static byte[] stringToByteArray(String original, boolean isHex)
    436     {
    437         int length = isHex ? original.length() / 2 : original.length();
    438         byte[] output = new byte[length];
    439         int i;
    440 
    441         if (isHex)
    442         {
    443             for (i = 0; i < length; i++)
    444             {
    445                 output[i] = (byte) Integer.parseInt(original.substring(i*2, i*2+2), 16);
    446             }
    447         }
    448         else {
    449             for (i = 0; i < length; i++)
    450             {
    451                 output[i] = (byte) original.charAt(i);
    452             }
    453         }
    454 
    455         return output;
    456     }
    457 
    458     /**
    459      * Unpacks an byte array containing 7-bit packed characters into a String.
    460      *
    461      * @param input a 7-bit packed char array
    462      * @return the unpacked String
    463      */
    464     static String decodeGSMPackedString(byte[] input)
    465     {
    466         final char PADDING_CHAR = 0x00;
    467         int lengthBytes = input.length;
    468         int lengthSeptets = (lengthBytes * 8) / 7;
    469         String decoded;
    470 
    471         /* Special case where the last 7 bits in the last byte could hold a valid
    472          * 7-bit character or a padding character. Drop the last 7-bit character
    473          * if it is a padding character.
    474          */
    475         if (lengthBytes % 7 == 0) {
    476             if (lengthBytes > 0) {
    477                 if ((input[lengthBytes - 1] >> 1) == PADDING_CHAR) {
    478                     lengthSeptets = lengthSeptets - 1;
    479                 }
    480             }
    481         }
    482 
    483         decoded = GsmAlphabet.gsm7BitPackedToString(input, 0, lengthSeptets);
    484 
    485         // Return "" if decoding of GSM packed string fails
    486         if (null == decoded) {
    487             Log.e(TAG, "Decoding of GSM packed string failed");
    488             decoded = "";
    489         }
    490 
    491         return decoded;
    492     }
    493 
    494     static String decodeUTF8String(byte[] input)
    495     {
    496         String decoded = "";
    497         try {
    498             decoded = new String(input, "UTF-8");
    499         }
    500         catch (UnsupportedEncodingException e)
    501         {
    502             throw new AssertionError();
    503         }
    504         return decoded;
    505     }
    506 
    507     static String decodeUCS2String(byte[] input)
    508     {
    509         String decoded = "";
    510         try {
    511             decoded = new String(input, "UTF-16");
    512         }
    513         catch (UnsupportedEncodingException e)
    514         {
    515             throw new AssertionError();
    516         }
    517         return decoded;
    518     }
    519 
    520     /** Decode NI string
    521      *
    522      * @param original   The text string to be decoded
    523      * @param isHex      Specifies whether the content of the string has been encoded as a Hex string. Encoding
    524      *                   a string as Hex can allow zeros inside the coded text.
    525      * @param coding     Specifies the coding scheme of the string, such as GSM, UTF8, UCS2, etc. This coding scheme
    526      *                      needs to match those used passed to HAL from the native GPS driver. Decoding is done according
    527      *                   to the <code> coding </code>, after a Hex string is decoded. Generally, if the
    528      *                   notification strings don't need further decoding, <code> coding </code> encoding can be
    529      *                   set to -1, and <code> isHex </code> can be false.
    530      * @return the decoded string
    531      */
    532     static private String decodeString(String original, boolean isHex, int coding)
    533     {
    534         String decoded = original;
    535         byte[] input = stringToByteArray(original, isHex);
    536 
    537         switch (coding) {
    538         case GPS_ENC_NONE:
    539             decoded = original;
    540             break;
    541 
    542         case GPS_ENC_SUPL_GSM_DEFAULT:
    543             decoded = decodeGSMPackedString(input);
    544             break;
    545 
    546         case GPS_ENC_SUPL_UTF8:
    547             decoded = decodeUTF8String(input);
    548             break;
    549 
    550         case GPS_ENC_SUPL_UCS2:
    551             decoded = decodeUCS2String(input);
    552             break;
    553 
    554         case GPS_ENC_UNKNOWN:
    555             decoded = original;
    556             break;
    557 
    558         default:
    559             Log.e(TAG, "Unknown encoding " + coding + " for NI text " + original);
    560             break;
    561         }
    562         return decoded;
    563     }
    564 
    565     // change this to configure notification display
    566     static private String getNotifTicker(GpsNiNotification notif, Context context)
    567     {
    568         String ticker = String.format(context.getString(R.string.gpsNotifTicker),
    569                 decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding),
    570                 decodeString(notif.text, mIsHexInput, notif.textEncoding));
    571         return ticker;
    572     }
    573 
    574     // change this to configure notification display
    575     static private String getNotifTitle(GpsNiNotification notif, Context context)
    576     {
    577         String title = String.format(context.getString(R.string.gpsNotifTitle));
    578         return title;
    579     }
    580 
    581     // change this to configure notification display
    582     static private String getNotifMessage(GpsNiNotification notif, Context context)
    583     {
    584         String message = String.format(context.getString(R.string.gpsNotifMessage),
    585                 decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding),
    586                 decodeString(notif.text, mIsHexInput, notif.textEncoding));
    587         return message;
    588     }
    589 
    590     // change this to configure dialog display (for verification)
    591     static public String getDialogTitle(GpsNiNotification notif, Context context)
    592     {
    593         return getNotifTitle(notif, context);
    594     }
    595 
    596     // change this to configure dialog display (for verification)
    597     static private String getDialogMessage(GpsNiNotification notif, Context context)
    598     {
    599         return getNotifMessage(notif, context);
    600     }
    601 
    602 }
    603