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