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.location.LocationManager;
     27 import android.os.Bundle;
     28 import android.os.RemoteException;
     29 import android.util.Log;
     30 
     31 /**
     32  * A GPS Network-initiated Handler class used by LocationManager.
     33  *
     34  * {@hide}
     35  */
     36 public 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         public int notificationId;
     98         public int niType;
     99         public boolean needNotify;
    100         public boolean needVerify;
    101         public boolean privacyOverride;
    102         public int timeout;
    103         public int defaultResponse;
    104         public String requestorId;
    105         public String text;
    106         public int requestorIdEncoding;
    107         public int textEncoding;
    108         public 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