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