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 import com.android.internal.R;
     32 import com.android.internal.telephony.GsmAlphabet;
     33 
     34 /**
     35  * A GPS Network-initiated Handler class used by LocationManager.
     36  *
     37  * {@hide}
     38  */
     39 public 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