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