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