blob: 190e1cc083bd23b8356f48f8005b6a4c4cc04aa6 [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.location;
import android.Manifest;
import android.annotation.RequiresPermission;
import android.app.Notification;
import android.app.NotificationManager;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.INetInitiatedListener;
import android.location.LocationManager;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import android.telephony.emergency.EmergencyNumber;
import android.util.Log;
import com.android.internal.R;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.telephony.GsmAlphabet;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.TimeUnit;
/**
* A GPS Network-initiated Handler class used by LocationManager.
*
* {@hide}
*/
public class GpsNetInitiatedHandler {
private static final String TAG = "GpsNetInitiatedHandler";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
// string constants for defining data fields in NI Intent
public static final String NI_INTENT_KEY_NOTIF_ID = "notif_id";
public static final String NI_INTENT_KEY_TITLE = "title";
public static final String NI_INTENT_KEY_MESSAGE = "message";
public static final String NI_INTENT_KEY_TIMEOUT = "timeout";
public static final String NI_INTENT_KEY_DEFAULT_RESPONSE = "default_resp";
// the extra command to send NI response to GnssLocationProvider
public static final String NI_RESPONSE_EXTRA_CMD = "send_ni_response";
// the extra command parameter names in the Bundle
public static final String NI_EXTRA_CMD_NOTIF_ID = "notif_id";
public static final String NI_EXTRA_CMD_RESPONSE = "response";
// these need to match GpsNiType constants in gps_ni.h
public static final int GPS_NI_TYPE_VOICE = 1;
public static final int GPS_NI_TYPE_UMTS_SUPL = 2;
public static final int GPS_NI_TYPE_UMTS_CTRL_PLANE = 3;
public static final int GPS_NI_TYPE_EMERGENCY_SUPL = 4;
// these need to match GpsUserResponseType constants in gps_ni.h
public static final int GPS_NI_RESPONSE_ACCEPT = 1;
public static final int GPS_NI_RESPONSE_DENY = 2;
public static final int GPS_NI_RESPONSE_NORESP = 3;
public static final int GPS_NI_RESPONSE_IGNORE = 4;
// these need to match GpsNiNotifyFlags constants in gps_ni.h
public static final int GPS_NI_NEED_NOTIFY = 0x0001;
public static final int GPS_NI_NEED_VERIFY = 0x0002;
public static final int GPS_NI_PRIVACY_OVERRIDE = 0x0004;
// these need to match GpsNiEncodingType in gps_ni.h
public static final int GPS_ENC_NONE = 0;
public static final int GPS_ENC_SUPL_GSM_DEFAULT = 1;
public static final int GPS_ENC_SUPL_UTF8 = 2;
public static final int GPS_ENC_SUPL_UCS2 = 3;
public static final int GPS_ENC_UNKNOWN = -1;
private final Context mContext;
private final TelephonyManager mTelephonyManager;
// parent gps location provider
private final LocationManager mLocationManager;
// configuration of notificaiton behavior
private boolean mPlaySounds = false;
private boolean mPopupImmediately = true;
// read the SUPL_ES form gps.conf
private volatile boolean mIsSuplEsEnabled;
// Set to true if the phone is having emergency call.
private volatile boolean mIsInEmergencyCall;
// If Location function is enabled.
private volatile boolean mIsLocationEnabled = false;
private final INetInitiatedListener mNetInitiatedListener;
// Set to true if string from HAL is encoded as Hex, e.g., "3F0039"
@UnsupportedAppUsage
static private boolean mIsHexInput = true;
// End time of emergency call, and extension, if set
private volatile long mCallEndElapsedRealtimeMillis = 0;
private volatile long mEmergencyExtensionMillis = 0;
public static class GpsNiNotification
{
@android.compat.annotation.UnsupportedAppUsage
public GpsNiNotification() {
}
public int notificationId;
public int niType;
public boolean needNotify;
public boolean needVerify;
public boolean privacyOverride;
public int timeout;
public int defaultResponse;
@UnsupportedAppUsage
public String requestorId;
@UnsupportedAppUsage
public String text;
@UnsupportedAppUsage
public int requestorIdEncoding;
@UnsupportedAppUsage
public int textEncoding;
}
private class EmergencyCallListener extends TelephonyCallback implements
TelephonyCallback.OutgoingEmergencyCallListener,
TelephonyCallback.CallStateListener {
@Override
@RequiresPermission(Manifest.permission.READ_ACTIVE_EMERGENCY_SESSION)
public void onOutgoingEmergencyCall(EmergencyNumber placedEmergencyNumber,
int subscriptionId) {
mIsInEmergencyCall = true;
if (DEBUG) Log.d(TAG, "onOutgoingEmergencyCall(): inEmergency = " + getInEmergency());
}
@Override
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
public void onCallStateChanged(int state) {
if (DEBUG) Log.d(TAG, "onCallStateChanged(): state is " + state);
// listening for emergency call ends
if (state == TelephonyManager.CALL_STATE_IDLE) {
if (mIsInEmergencyCall) {
mCallEndElapsedRealtimeMillis = SystemClock.elapsedRealtime();
mIsInEmergencyCall = false;
}
}
}
}
// The internal implementation of TelephonyManager uses WeakReference so we have to keep a
// reference here.
private final EmergencyCallListener mEmergencyCallListener = new EmergencyCallListener();
/**
* The notification that is shown when a network-initiated notification
* (and verification) event is received.
* <p>
* This is lazily created, so use {@link #setNINotification()}.
*/
private Notification.Builder mNiNotificationBuilder;
public GpsNetInitiatedHandler(Context context,
INetInitiatedListener netInitiatedListener,
boolean isSuplEsEnabled) {
mContext = context;
if (netInitiatedListener == null) {
throw new IllegalArgumentException("netInitiatedListener is null");
} else {
mNetInitiatedListener = netInitiatedListener;
}
setSuplEsEnabled(isSuplEsEnabled);
mLocationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
updateLocationMode();
mTelephonyManager =
(TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
mTelephonyManager.registerTelephonyCallback(mContext.getMainExecutor(),
mEmergencyCallListener);
BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(LocationManager.MODE_CHANGED_ACTION)) {
updateLocationMode();
if (DEBUG) Log.d(TAG, "location enabled :" + getLocationEnabled());
}
}
};
mContext.registerReceiver(broadcastReceiver,
new IntentFilter(LocationManager.MODE_CHANGED_ACTION));
}
public void setSuplEsEnabled(boolean isEnabled) {
mIsSuplEsEnabled = isEnabled;
}
public boolean getSuplEsEnabled() {
return mIsSuplEsEnabled;
}
/**
* Updates Location enabler based on location setting.
*/
public void updateLocationMode() {
mIsLocationEnabled = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
}
/**
* Checks if user agreed to use location.
*/
public boolean getLocationEnabled() {
return mIsLocationEnabled;
}
/**
* Determines whether device is in user-initiated emergency session based on the following
* 1. If the user is making an emergency call, this is provided by actively
* monitoring the outgoing phone number;
* 2. If the user has recently ended an emergency call, and the device is in a configured time
* window after the end of that call.
* 3. If the device is in a emergency callback state, this is provided by querying
* TelephonyManager.
* 4. If the user has recently sent an Emergency SMS and telephony reports that it is in
* emergency SMS mode, this is provided by querying TelephonyManager.
* @return true if is considered in user initiated emergency mode for NI purposes
*/
public boolean getInEmergency() {
return getInEmergency(mEmergencyExtensionMillis);
}
/**
* Determines whether device is in user-initiated emergency session with the given extension
* time.
*
* @return true if is considered in user initiated emergency mode for NI purposes within the
* given extension time.
*
* @see {@link #getInEmergency()}
*/
public boolean getInEmergency(long emergencyExtensionMillis) {
boolean isInEmergencyExtension =
(mCallEndElapsedRealtimeMillis > 0)
&& ((SystemClock.elapsedRealtime() - mCallEndElapsedRealtimeMillis)
< emergencyExtensionMillis);
boolean isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
boolean isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
return mIsInEmergencyCall || isInEmergencyCallback || isInEmergencyExtension
|| isInEmergencySmsMode;
}
public void setEmergencyExtensionSeconds(int emergencyExtensionSeconds) {
mEmergencyExtensionMillis = TimeUnit.SECONDS.toMillis(emergencyExtensionSeconds);
}
// Handles NI events from HAL
@UnsupportedAppUsage
public void handleNiNotification(GpsNiNotification notif) {
if (DEBUG) Log.d(TAG, "in handleNiNotification () :"
+ " notificationId: " + notif.notificationId
+ " requestorId: " + notif.requestorId
+ " text: " + notif.text
+ " mIsSuplEsEnabled" + getSuplEsEnabled()
+ " mIsLocationEnabled" + getLocationEnabled());
if (getSuplEsEnabled()) {
handleNiInEs(notif);
} else {
handleNi(notif);
}
//////////////////////////////////////////////////////////////////////////
// A note about timeout
// According to the protocol, in the need_notify and need_verify case,
// a default response should be sent when time out.
//
// In some GPS hardware, the GPS driver (under HAL) can handle the timeout case
// and this class GpsNetInitiatedHandler does not need to do anything.
//
// However, the UI should at least close the dialog when timeout. Further,
// for more general handling, timeout response should be added to the Handler here.
//
}
// handle NI form HAL when SUPL_ES is disabled.
private void handleNi(GpsNiNotification notif) {
if (DEBUG) Log.d(TAG, "in handleNi () :"
+ " needNotify: " + notif.needNotify
+ " needVerify: " + notif.needVerify
+ " privacyOverride: " + notif.privacyOverride
+ " mPopupImmediately: " + mPopupImmediately
+ " mInEmergency: " + getInEmergency());
if (!getLocationEnabled() && !getInEmergency()) {
// Location is currently disabled, ignore all NI requests.
try {
mNetInitiatedListener.sendNiResponse(notif.notificationId,
GPS_NI_RESPONSE_IGNORE);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in sendNiResponse");
}
}
if (notif.needNotify) {
// If NI does not need verify or the dialog is not requested
// to pop up immediately, the dialog box will not pop up.
if (notif.needVerify && mPopupImmediately) {
// Popup the dialog box now
openNiDialog(notif);
} else {
// Show the notification
setNiNotification(notif);
}
}
// ACCEPT cases: 1. Notify, no verify; 2. no notify, no verify;
// 3. privacy override.
if (!notif.needVerify || notif.privacyOverride) {
try {
mNetInitiatedListener.sendNiResponse(notif.notificationId,
GPS_NI_RESPONSE_ACCEPT);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in sendNiResponse");
}
}
}
// handle NI from HAL when the SUPL_ES is enabled
private void handleNiInEs(GpsNiNotification notif) {
if (DEBUG) Log.d(TAG, "in handleNiInEs () :"
+ " niType: " + notif.niType
+ " notificationId: " + notif.notificationId);
// UE is in emergency mode when in emergency call mode or in emergency call back mode
/*
1. When SUPL ES bit is off and UE is not in emergency mode:
Call handleNi() to do legacy behaviour.
2. When SUPL ES bit is on and UE is in emergency mode:
Call handleNi() to do acceptance behaviour.
3. When SUPL ES bit is off but UE is in emergency mode:
Ignore the emergency SUPL INIT.
4. When SUPL ES bit is on but UE is not in emergency mode:
Ignore the emergency SUPL INIT.
*/
boolean isNiTypeES = (notif.niType == GPS_NI_TYPE_EMERGENCY_SUPL);
if (isNiTypeES != getInEmergency()) {
try {
mNetInitiatedListener.sendNiResponse(notif.notificationId,
GPS_NI_RESPONSE_IGNORE);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in sendNiResponse");
}
} else {
handleNi(notif);
}
}
/**
* Posts a notification in the status bar using the contents in {@code notif} object.
*/
private synchronized void setNiNotification(GpsNiNotification notif) {
NotificationManager notificationManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager == null) {
return;
}
String title = getNotifTitle(notif, mContext);
String message = getNotifMessage(notif, mContext);
if (DEBUG) Log.d(TAG, "setNiNotification, notifyId: " + notif.notificationId +
", title: " + title +
", message: " + message);
// Construct Notification
if (mNiNotificationBuilder == null) {
mNiNotificationBuilder = new Notification.Builder(mContext,
SystemNotificationChannels.NETWORK_ALERTS)
.setSmallIcon(com.android.internal.R.drawable.stat_sys_gps_on)
.setWhen(0)
.setOngoing(true)
.setAutoCancel(true)
.setColor(mContext.getColor(
com.android.internal.R.color.system_notification_accent_color));
}
if (mPlaySounds) {
mNiNotificationBuilder.setDefaults(Notification.DEFAULT_SOUND);
} else {
mNiNotificationBuilder.setDefaults(0);
}
mNiNotificationBuilder.setTicker(getNotifTicker(notif, mContext))
.setContentTitle(title)
.setContentText(message);
notificationManager.notifyAsUser(null, notif.notificationId, mNiNotificationBuilder.build(),
UserHandle.ALL);
}
// Opens the notification dialog and waits for user input
private void openNiDialog(GpsNiNotification notif)
{
Intent intent = getDlgIntent(notif);
if (DEBUG) Log.d(TAG, "openNiDialog, notifyId: " + notif.notificationId +
", requestorId: " + notif.requestorId +
", text: " + notif.text);
mContext.startActivity(intent);
}
// Construct the intent for bringing up the dialog activity, which shows the
// notification and takes user input
private Intent getDlgIntent(GpsNiNotification notif)
{
Intent intent = new Intent();
String title = getDialogTitle(notif, mContext);
String message = getDialogMessage(notif, mContext);
// directly bring up the NI activity
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.setClass(mContext, com.android.internal.app.NetInitiatedActivity.class);
// put data in the intent
intent.putExtra(NI_INTENT_KEY_NOTIF_ID, notif.notificationId);
intent.putExtra(NI_INTENT_KEY_TITLE, title);
intent.putExtra(NI_INTENT_KEY_MESSAGE, message);
intent.putExtra(NI_INTENT_KEY_TIMEOUT, notif.timeout);
intent.putExtra(NI_INTENT_KEY_DEFAULT_RESPONSE, notif.defaultResponse);
if (DEBUG) Log.d(TAG, "generateIntent, title: " + title + ", message: " + message +
", timeout: " + notif.timeout);
return intent;
}
// Converts a string (or Hex string) to a char array
static byte[] stringToByteArray(String original, boolean isHex)
{
int length = isHex ? original.length() / 2 : original.length();
byte[] output = new byte[length];
int i;
if (isHex)
{
for (i = 0; i < length; i++)
{
output[i] = (byte) Integer.parseInt(original.substring(i*2, i*2+2), 16);
}
}
else {
for (i = 0; i < length; i++)
{
output[i] = (byte) original.charAt(i);
}
}
return output;
}
/**
* Unpacks an byte array containing 7-bit packed characters into a String.
*
* @param input a 7-bit packed char array
* @return the unpacked String
*/
static String decodeGSMPackedString(byte[] input)
{
final char PADDING_CHAR = 0x00;
int lengthBytes = input.length;
int lengthSeptets = (lengthBytes * 8) / 7;
String decoded;
/* Special case where the last 7 bits in the last byte could hold a valid
* 7-bit character or a padding character. Drop the last 7-bit character
* if it is a padding character.
*/
if (lengthBytes % 7 == 0) {
if (lengthBytes > 0) {
if ((input[lengthBytes - 1] >> 1) == PADDING_CHAR) {
lengthSeptets = lengthSeptets - 1;
}
}
}
decoded = GsmAlphabet.gsm7BitPackedToString(input, 0, lengthSeptets);
// Return "" if decoding of GSM packed string fails
if (null == decoded) {
Log.e(TAG, "Decoding of GSM packed string failed");
decoded = "";
}
return decoded;
}
static String decodeUTF8String(byte[] input)
{
String decoded = "";
try {
decoded = new String(input, "UTF-8");
}
catch (UnsupportedEncodingException e)
{
throw new AssertionError();
}
return decoded;
}
static String decodeUCS2String(byte[] input)
{
String decoded = "";
try {
decoded = new String(input, "UTF-16");
}
catch (UnsupportedEncodingException e)
{
throw new AssertionError();
}
return decoded;
}
/** Decode NI string
*
* @param original The text string to be decoded
* @param isHex Specifies whether the content of the string has been encoded as a Hex string. Encoding
* a string as Hex can allow zeros inside the coded text.
* @param coding Specifies the coding scheme of the string, such as GSM, UTF8, UCS2, etc. This coding scheme
* needs to match those used passed to HAL from the native GPS driver. Decoding is done according
* to the <code> coding </code>, after a Hex string is decoded. Generally, if the
* notification strings don't need further decoding, <code> coding </code> encoding can be
* set to -1, and <code> isHex </code> can be false.
* @return the decoded string
*/
@UnsupportedAppUsage
static private String decodeString(String original, boolean isHex, int coding)
{
if (coding == GPS_ENC_NONE || coding == GPS_ENC_UNKNOWN) {
return original;
}
byte[] input = stringToByteArray(original, isHex);
switch (coding) {
case GPS_ENC_SUPL_GSM_DEFAULT:
return decodeGSMPackedString(input);
case GPS_ENC_SUPL_UTF8:
return decodeUTF8String(input);
case GPS_ENC_SUPL_UCS2:
return decodeUCS2String(input);
default:
Log.e(TAG, "Unknown encoding " + coding + " for NI text " + original);
return original;
}
}
// change this to configure notification display
static private String getNotifTicker(GpsNiNotification notif, Context context)
{
String ticker = String.format(context.getString(R.string.gpsNotifTicker),
decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding),
decodeString(notif.text, mIsHexInput, notif.textEncoding));
return ticker;
}
// change this to configure notification display
static private String getNotifTitle(GpsNiNotification notif, Context context)
{
String title = String.format(context.getString(R.string.gpsNotifTitle));
return title;
}
// change this to configure notification display
static private String getNotifMessage(GpsNiNotification notif, Context context)
{
String message = String.format(context.getString(R.string.gpsNotifMessage),
decodeString(notif.requestorId, mIsHexInput, notif.requestorIdEncoding),
decodeString(notif.text, mIsHexInput, notif.textEncoding));
return message;
}
// change this to configure dialog display (for verification)
static public String getDialogTitle(GpsNiNotification notif, Context context)
{
return getNotifTitle(notif, context);
}
// change this to configure dialog display (for verification)
static private String getDialogMessage(GpsNiNotification notif, Context context)
{
return getNotifMessage(notif, context);
}
}