GpsNetInitiatedHandler.java revision 40c5ccfffe1aca40c8e6b9155ce2f9918b53701a
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.Context;
25import android.content.Intent;
26import android.location.LocationManager;
27import android.os.Bundle;
28import android.os.RemoteException;
29import android.util.Log;
30
31import com.android.internal.R;
32import com.android.internal.telephony.GsmAlphabet;
33
34/**
35 * A GPS Network-initiated Handler class used by LocationManager.
36 *
37 * {@hide}
38 */
39public class GpsNetInitiatedHandler {
40
41    private static final String TAG = "GpsNetInitiatedHandler";
42
43    private static final boolean DEBUG = true;
44    private static final boolean VERBOSE = false;
45
46    // NI verify activity for bringing up UI (not used yet)
47    public static final String ACTION_NI_VERIFY = "android.intent.action.NETWORK_INITIATED_VERIFY";
48
49    // string constants for defining data fields in NI Intent
50    public static final String NI_INTENT_KEY_NOTIF_ID = "notif_id";
51    public static final String NI_INTENT_KEY_TITLE = "title";
52    public static final String NI_INTENT_KEY_MESSAGE = "message";
53    public static final String NI_INTENT_KEY_TIMEOUT = "timeout";
54    public static final String NI_INTENT_KEY_DEFAULT_RESPONSE = "default_resp";
55
56    // the extra command to send NI response to GpsLocationProvider
57    public static final String NI_RESPONSE_EXTRA_CMD = "send_ni_response";
58
59    // the extra command parameter names in the Bundle
60    public static final String NI_EXTRA_CMD_NOTIF_ID = "notif_id";
61    public static final String NI_EXTRA_CMD_RESPONSE = "response";
62
63    // these need to match GpsNiType constants in gps_ni.h
64    public static final int GPS_NI_TYPE_VOICE = 1;
65    public static final int GPS_NI_TYPE_UMTS_SUPL = 2;
66    public static final int GPS_NI_TYPE_UMTS_CTRL_PLANE = 3;
67
68    // these need to match GpsUserResponseType constants in gps_ni.h
69    public static final int GPS_NI_RESPONSE_ACCEPT = 1;
70    public static final int GPS_NI_RESPONSE_DENY = 2;
71    public static final int GPS_NI_RESPONSE_NORESP = 3;
72
73    // these need to match GpsNiNotifyFlags constants in gps_ni.h
74    public static final int GPS_NI_NEED_NOTIFY = 0x0001;
75    public static final int GPS_NI_NEED_VERIFY = 0x0002;
76    public static final int GPS_NI_PRIVACY_OVERRIDE = 0x0004;
77
78    // these need to match GpsNiEncodingType in gps_ni.h
79    public static final int GPS_ENC_NONE = 0;
80    public static final int GPS_ENC_SUPL_GSM_DEFAULT = 1;
81    public static final int GPS_ENC_SUPL_UTF8 = 2;
82    public static final int GPS_ENC_SUPL_UCS2 = 3;
83    public static final int GPS_ENC_UNKNOWN = -1;
84
85    private final Context mContext;
86
87    // parent gps location provider
88    private final LocationManager mLocationManager;
89
90    // configuration of notificaiton behavior
91    private boolean mPlaySounds = false;
92    private boolean visible = true;
93    private boolean mPopupImmediately = true;
94
95    // Set to true if string from HAL is encoded as Hex, e.g., "3F0039"
96    static private boolean mIsHexInput = true;
97
98    public static class GpsNiNotification
99    {
100        public int notificationId;
101        public int niType;
102        public boolean needNotify;
103        public boolean needVerify;
104        public boolean privacyOverride;
105        public int timeout;
106        public int defaultResponse;
107        public String requestorId;
108        public String text;
109        public int requestorIdEncoding;
110        public int textEncoding;
111        public Bundle extras;
112    };
113
114    public static class GpsNiResponse {
115        /* User reponse, one of the values in GpsUserResponseType */
116        int userResponse;
117        /* Optional extra data to pass with the user response */
118        Bundle extras;
119    };
120
121    /**
122     * The notification that is shown when a network-initiated notification
123     * (and verification) event is received.
124     * <p>
125     * This is lazily created, so use {@link #setNINotification()}.
126     */
127    private Notification mNiNotification;
128
129    public GpsNetInitiatedHandler(Context context) {
130        mContext = context;
131        mLocationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
132    }
133
134    // Handles NI events from HAL
135    public void handleNiNotification(GpsNiNotification notif)
136    {
137        if (DEBUG) Log.d(TAG, "handleNiNotification" + " notificationId: " + notif.notificationId
138                + " requestorId: " + notif.requestorId + " text: " + notif.text);
139
140        // Notify and verify with immediate pop-up
141        if (notif.needNotify && notif.needVerify && mPopupImmediately)
142        {
143            // Popup the dialog box now
144            openNiDialog(notif);
145        }
146
147        // Notify only, or delayed pop-up (change mPopupImmediately to FALSE)
148        if (notif.needNotify && !notif.needVerify ||
149            notif.needNotify && notif.needVerify && !mPopupImmediately)
150        {
151            // Show the notification
152
153            // if mPopupImmediately == FALSE and needVerify == TRUE, a dialog will be opened
154            // when the user opens the notification message
155
156            setNiNotification(notif);
157        }
158
159        // ACCEPT cases: 1. Notify, no verify; 2. no notify, no verify; 3. privacy override.
160        if ( notif.needNotify && !notif.needVerify ||
161            !notif.needNotify && !notif.needVerify ||
162             notif.privacyOverride)
163        {
164            mLocationManager.sendNiResponse(notif.notificationId, GPS_NI_RESPONSE_ACCEPT);
165        }
166
167        //////////////////////////////////////////////////////////////////////////
168        //   A note about timeout
169        //   According to the protocol, in the need_notify and need_verify case,
170        //   a default response should be sent when time out.
171        //
172        //   In some GPS hardware, the GPS driver (under HAL) can handle the timeout case
173        //   and this class GpsNetInitiatedHandler does not need to do anything.
174        //
175        //   However, the UI should at least close the dialog when timeout. Further,
176        //   for more general handling, timeout response should be added to the Handler here.
177        //
178    }
179
180    // Sets the NI notification.
181    private synchronized void setNiNotification(GpsNiNotification notif) {
182        NotificationManager notificationManager = (NotificationManager) mContext
183                .getSystemService(Context.NOTIFICATION_SERVICE);
184        if (notificationManager == null) {
185            return;
186        }
187
188        String title = getNotifTitle(notif, mContext);
189        String message = getNotifMessage(notif, mContext);
190
191        if (DEBUG) Log.d(TAG, "setNiNotification, notifyId: " + notif.notificationId +
192                ", title: " + title +
193                ", message: " + message);
194
195        // Construct Notification
196        if (mNiNotification == null) {
197            mNiNotification = new Notification();
198            mNiNotification.icon = com.android.internal.R.drawable.stat_sys_gps_on; /* Change notification icon here */
199            mNiNotification.when = 0;
200        }
201
202        if (mPlaySounds) {
203            mNiNotification.defaults |= Notification.DEFAULT_SOUND;
204        } else {
205            mNiNotification.defaults &= ~Notification.DEFAULT_SOUND;
206        }
207
208        mNiNotification.flags = Notification.FLAG_ONGOING_EVENT | Notification.FLAG_AUTO_CANCEL;
209        mNiNotification.tickerText = getNotifTicker(notif, mContext);
210
211        // if not to popup dialog immediately, pending intent will open the dialog
212        Intent intent = !mPopupImmediately ? getDlgIntent(notif) : new Intent();
213        PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0);
214        mNiNotification.setLatestEventInfo(mContext, title, message, pi);
215
216        if (visible) {
217            notificationManager.notify(notif.notificationId, mNiNotification);
218        } else {
219            notificationManager.cancel(notif.notificationId);
220        }
221    }
222
223    // Opens the notification dialog and waits for user input
224    private void openNiDialog(GpsNiNotification notif)
225    {
226        Intent intent = getDlgIntent(notif);
227
228        if (DEBUG) Log.d(TAG, "openNiDialog, notifyId: " + notif.notificationId +
229                ", requestorId: " + notif.requestorId +
230                ", text: " + notif.text);
231
232        mContext.startActivity(intent);
233    }
234
235    // Construct the intent for bringing up the dialog activity, which shows the
236    // notification and takes user input
237    private Intent getDlgIntent(GpsNiNotification notif)
238    {
239        Intent intent = new Intent();
240        String title = getDialogTitle(notif, mContext);
241        String message = getDialogMessage(notif, mContext);
242
243        // directly bring up the NI activity
244        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
245        intent.setClass(mContext, com.android.internal.app.NetInitiatedActivity.class);
246
247        // put data in the intent
248        intent.putExtra(NI_INTENT_KEY_NOTIF_ID, notif.notificationId);
249        intent.putExtra(NI_INTENT_KEY_TITLE, title);
250        intent.putExtra(NI_INTENT_KEY_MESSAGE, message);
251        intent.putExtra(NI_INTENT_KEY_TIMEOUT, notif.timeout);
252        intent.putExtra(NI_INTENT_KEY_DEFAULT_RESPONSE, notif.defaultResponse);
253
254        if (DEBUG) Log.d(TAG, "generateIntent, title: " + title + ", message: " + message +
255                ", timeout: " + notif.timeout);
256
257        return intent;
258    }
259
260    // Converts a string (or Hex string) to a char array
261    static byte[] stringToByteArray(String original, boolean isHex)
262    {
263        int length = isHex ? original.length() / 2 : original.length();
264        byte[] output = new byte[length];
265        int i;
266
267        if (isHex)
268        {
269            for (i = 0; i < length; i++)
270            {
271                output[i] = (byte) Integer.parseInt(original.substring(i*2, i*2+2), 16);
272            }
273        }
274        else {
275            for (i = 0; i < length; i++)
276            {
277                output[i] = (byte) original.charAt(i);
278            }
279        }
280
281        return output;
282    }
283
284    /**
285     * Unpacks an byte array containing 7-bit packed characters into a String.
286     *
287     * @param input a 7-bit packed char array
288     * @return the unpacked String
289     */
290    static String decodeGSMPackedString(byte[] input)
291    {
292        final char PADDING_CHAR = 0x00;
293        int lengthBytes = input.length;
294        int lengthSeptets = (lengthBytes * 8) / 7;
295        String decoded;
296
297        /* Special case where the last 7 bits in the last byte could hold a valid
298         * 7-bit character or a padding character. Drop the last 7-bit character
299         * if it is a padding character.
300         */
301        if (lengthBytes % 7 == 0) {
302            if (lengthBytes > 0) {
303                if ((input[lengthBytes - 1] >> 1) == PADDING_CHAR) {
304                    lengthSeptets = lengthSeptets - 1;
305                }
306            }
307        }
308
309        decoded = GsmAlphabet.gsm7BitPackedToString(input, 0, lengthSeptets);
310
311        // Return "" if decoding of GSM packed string fails
312        if (null == decoded) {
313            Log.e(TAG, "Decoding of GSM packed string failed");
314            decoded = "";
315        }
316
317        return decoded;
318    }
319
320    static String decodeUTF8String(byte[] input)
321    {
322        String decoded = "";
323        try {
324            decoded = new String(input, "UTF-8");
325        }
326        catch (UnsupportedEncodingException e)
327        {
328            throw new AssertionError();
329        }
330        return decoded;
331    }
332
333    static String decodeUCS2String(byte[] input)
334    {
335        String decoded = "";
336        try {
337            decoded = new String(input, "UTF-16");
338        }
339        catch (UnsupportedEncodingException e)
340        {
341            throw new AssertionError();
342        }
343        return decoded;
344    }
345
346    /** Decode NI string
347     *
348     * @param original   The text string to be decoded
349     * @param isHex      Specifies whether the content of the string has been encoded as a Hex string. Encoding
350     *                   a string as Hex can allow zeros inside the coded text.
351     * @param coding     Specifies the coding scheme of the string, such as GSM, UTF8, UCS2, etc. This coding scheme
352     *                      needs to match those used passed to HAL from the native GPS driver. Decoding is done according
353     *                   to the <code> coding </code>, after a Hex string is decoded. Generally, if the
354     *                   notification strings don't need further decoding, <code> coding </code> encoding can be
355     *                   set to -1, and <code> isHex </code> can be false.
356     * @return the decoded string
357     */
358    static private String decodeString(String original, boolean isHex, int coding)
359    {
360        String decoded = original;
361        byte[] input = stringToByteArray(original, isHex);
362
363        switch (coding) {
364        case GPS_ENC_NONE:
365            decoded = original;
366            break;
367
368        case GPS_ENC_SUPL_GSM_DEFAULT:
369            decoded = decodeGSMPackedString(input);
370            break;
371
372        case GPS_ENC_SUPL_UTF8:
373            decoded = decodeUTF8String(input);
374            break;
375
376        case GPS_ENC_SUPL_UCS2:
377            decoded = decodeUCS2String(input);
378            break;
379
380        case GPS_ENC_UNKNOWN:
381            decoded = original;
382            break;
383
384        default:
385            Log.e(TAG, "Unknown encoding " + coding + " for NI text " + original);
386            break;
387        }
388        return decoded;
389    }
390
391    // change this to configure notification display
392    static private String getNotifTicker(GpsNiNotification notif, Context context)
393    {
394        String ticker = String.format(context.getString(R.string.gpsNotifTicker),
395                decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding),
396                decodeString(notif.text, mIsHexInput, notif.textEncoding));
397        return ticker;
398    }
399
400    // change this to configure notification display
401    static private String getNotifTitle(GpsNiNotification notif, Context context)
402    {
403        String title = String.format(context.getString(R.string.gpsNotifTitle));
404        return title;
405    }
406
407    // change this to configure notification display
408    static private String getNotifMessage(GpsNiNotification notif, Context context)
409    {
410        String message = String.format(context.getString(R.string.gpsNotifMessage),
411                decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding),
412                decodeString(notif.text, mIsHexInput, notif.textEncoding));
413        return message;
414    }
415
416    // change this to configure dialog display (for verification)
417    static public String getDialogTitle(GpsNiNotification notif, Context context)
418    {
419        return getNotifTitle(notif, context);
420    }
421
422    // change this to configure dialog display (for verification)
423    static private String getDialogMessage(GpsNiNotification notif, Context context)
424    {
425        return getNotifMessage(notif, context);
426    }
427
428}
429