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