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 GnssLocationProvider
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 
GpsNetInitiatedHandler(Context context, INetInitiatedListener netInitiatedListener, boolean isSuplEsEnabled)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 
setSuplEsEnabled(boolean isEnabled)212     public void setSuplEsEnabled(boolean isEnabled) {
213         mIsSuplEsEnabled = isEnabled;
214     }
215 
getSuplEsEnabled()216     public boolean getSuplEsEnabled() {
217         return mIsSuplEsEnabled;
218     }
219 
220     /**
221      * Updates Location enabler based on location setting.
222      */
updateLocationMode()223     public void updateLocationMode() {
224         mIsLocationEnabled = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
225     }
226 
227     /**
228      * Checks if user agreed to use location.
229      */
getLocationEnabled()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.
setInEmergency(boolean isInEmergency)243     public void setInEmergency(boolean isInEmergency) {
244         mIsInEmergency = isInEmergency;
245     }
246 
getInEmergency()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
handleNiNotification(GpsNiNotification notif)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.
handleNi(GpsNiNotification notif)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
handleNiInEs(GpsNiNotification notif)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.
setNiNotification(GpsNiNotification notif)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
openNiDialog(GpsNiNotification notif)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
getDlgIntent(GpsNiNotification notif)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
stringToByteArray(String original, boolean isHex)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      */
decodeGSMPackedString(byte[] input)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 
decodeUTF8String(byte[] input)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 
decodeUCS2String(byte[] input)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      */
decodeString(String original, boolean isHex, int coding)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
getNotifTicker(GpsNiNotification notif, Context context)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
getNotifTitle(GpsNiNotification notif, Context context)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
getNotifMessage(GpsNiNotification notif, Context context)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)
getDialogTitle(GpsNiNotification notif, Context context)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)
getDialogMessage(GpsNiNotification notif, Context context)598     static private String getDialogMessage(GpsNiNotification notif, Context context)
599     {
600         return getNotifMessage(notif, context);
601     }
602 
603 }
604