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