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