blob: 441270a28b939f977b09a421c007adf686cc8b11 [file] [log] [blame]
/*
* Copyright (C) 2006 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.phone;
import android.app.ActivityManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.AsyncResult;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.telephony.NeighboringCellInfo;
import android.telephony.CellInfo;
import android.telephony.ServiceState;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.telephony.DefaultPhoneNotifier;
import com.android.internal.telephony.IccCard;
import com.android.internal.telephony.ITelephony;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.CallManager;
import com.android.internal.telephony.PhoneConstants;
import java.util.List;
import java.util.ArrayList;
/**
* Implementation of the ITelephony interface.
*/
public class PhoneInterfaceManager extends ITelephony.Stub {
private static final String LOG_TAG = "PhoneInterfaceManager";
private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
private static final boolean DBG_LOC = false;
// Message codes used with mMainThreadHandler
private static final int CMD_HANDLE_PIN_MMI = 1;
private static final int CMD_HANDLE_NEIGHBORING_CELL = 2;
private static final int EVENT_NEIGHBORING_CELL_DONE = 3;
private static final int CMD_ANSWER_RINGING_CALL = 4;
private static final int CMD_END_CALL = 5; // not used yet
private static final int CMD_SILENCE_RINGER = 6;
/** The singleton instance. */
private static PhoneInterfaceManager sInstance;
PhoneGlobals mApp;
Phone mPhone;
CallManager mCM;
MainThreadHandler mMainThreadHandler;
/**
* A request object for use with {@link MainThreadHandler}. Requesters should wait() on the
* request after sending. The main thread will notify the request when it is complete.
*/
private static final class MainThreadRequest {
/** The argument to use for the request */
public Object argument;
/** The result of the request that is run on the main thread */
public Object result;
public MainThreadRequest(Object argument) {
this.argument = argument;
}
}
/**
* A handler that processes messages on the main thread in the phone process. Since many
* of the Phone calls are not thread safe this is needed to shuttle the requests from the
* inbound binder threads to the main thread in the phone process. The Binder thread
* may provide a {@link MainThreadRequest} object in the msg.obj field that they are waiting
* on, which will be notified when the operation completes and will contain the result of the
* request.
*
* <p>If a MainThreadRequest object is provided in the msg.obj field,
* note that request.result must be set to something non-null for the calling thread to
* unblock.
*/
private final class MainThreadHandler extends Handler {
@Override
public void handleMessage(Message msg) {
MainThreadRequest request;
Message onCompleted;
AsyncResult ar;
switch (msg.what) {
case CMD_HANDLE_PIN_MMI:
request = (MainThreadRequest) msg.obj;
request.result = Boolean.valueOf(
mPhone.handlePinMmi((String) request.argument));
// Wake up the requesting thread
synchronized (request) {
request.notifyAll();
}
break;
case CMD_HANDLE_NEIGHBORING_CELL:
request = (MainThreadRequest) msg.obj;
onCompleted = obtainMessage(EVENT_NEIGHBORING_CELL_DONE,
request);
mPhone.getNeighboringCids(onCompleted);
break;
case EVENT_NEIGHBORING_CELL_DONE:
ar = (AsyncResult) msg.obj;
request = (MainThreadRequest) ar.userObj;
if (ar.exception == null && ar.result != null) {
request.result = ar.result;
} else {
// create an empty list to notify the waiting thread
request.result = new ArrayList<NeighboringCellInfo>();
}
// Wake up the requesting thread
synchronized (request) {
request.notifyAll();
}
break;
case CMD_ANSWER_RINGING_CALL:
answerRingingCallInternal();
break;
case CMD_SILENCE_RINGER:
silenceRingerInternal();
break;
case CMD_END_CALL:
request = (MainThreadRequest) msg.obj;
boolean hungUp = false;
int phoneType = mPhone.getPhoneType();
if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
// CDMA: If the user presses the Power button we treat it as
// ending the complete call session
hungUp = PhoneUtils.hangupRingingAndActive(mPhone);
} else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
// GSM: End the call as per the Phone state
hungUp = PhoneUtils.hangup(mCM);
} else {
throw new IllegalStateException("Unexpected phone type: " + phoneType);
}
if (DBG) log("CMD_END_CALL: " + (hungUp ? "hung up!" : "no call to hang up"));
request.result = hungUp;
// Wake up the requesting thread
synchronized (request) {
request.notifyAll();
}
break;
default:
Log.w(LOG_TAG, "MainThreadHandler: unexpected message code: " + msg.what);
break;
}
}
}
/**
* Posts the specified command to be executed on the main thread,
* waits for the request to complete, and returns the result.
* @see sendRequestAsync
*/
private Object sendRequest(int command, Object argument) {
if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
throw new RuntimeException("This method will deadlock if called from the main thread.");
}
MainThreadRequest request = new MainThreadRequest(argument);
Message msg = mMainThreadHandler.obtainMessage(command, request);
msg.sendToTarget();
// Wait for the request to complete
synchronized (request) {
while (request.result == null) {
try {
request.wait();
} catch (InterruptedException e) {
// Do nothing, go back and wait until the request is complete
}
}
}
return request.result;
}
/**
* Asynchronous ("fire and forget") version of sendRequest():
* Posts the specified command to be executed on the main thread, and
* returns immediately.
* @see sendRequest
*/
private void sendRequestAsync(int command) {
mMainThreadHandler.sendEmptyMessage(command);
}
/**
* Initialize the singleton PhoneInterfaceManager instance.
* This is only done once, at startup, from PhoneApp.onCreate().
*/
/* package */ static PhoneInterfaceManager init(PhoneGlobals app, Phone phone) {
synchronized (PhoneInterfaceManager.class) {
if (sInstance == null) {
sInstance = new PhoneInterfaceManager(app, phone);
} else {
Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance);
}
return sInstance;
}
}
/** Private constructor; @see init() */
private PhoneInterfaceManager(PhoneGlobals app, Phone phone) {
mApp = app;
mPhone = phone;
mCM = PhoneGlobals.getInstance().mCM;
mMainThreadHandler = new MainThreadHandler();
publish();
}
private void publish() {
if (DBG) log("publish: " + this);
ServiceManager.addService("phone", this);
}
//
// Implementation of the ITelephony interface.
//
public void dial(String number) {
if (DBG) log("dial: " + number);
// No permission check needed here: This is just a wrapper around the
// ACTION_DIAL intent, which is available to any app since it puts up
// the UI before it does anything.
String url = createTelUrl(number);
if (url == null) {
return;
}
// PENDING: should we just silently fail if phone is offhook or ringing?
PhoneConstants.State state = mCM.getState();
if (state != PhoneConstants.State.OFFHOOK && state != PhoneConstants.State.RINGING) {
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(url));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mApp.startActivity(intent);
}
}
public void call(String number) {
if (DBG) log("call: " + number);
// This is just a wrapper around the ACTION_CALL intent, but we still
// need to do a permission check since we're calling startActivity()
// from the context of the phone app.
enforceCallPermission();
String url = createTelUrl(number);
if (url == null) {
return;
}
Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse(url));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mApp.startActivity(intent);
}
private boolean showCallScreenInternal(boolean specifyInitialDialpadState,
boolean initialDialpadState) {
if (!PhoneGlobals.sVoiceCapable) {
// Never allow the InCallScreen to appear on data-only devices.
return false;
}
if (isIdle()) {
return false;
}
// If the phone isn't idle then go to the in-call screen
long callingId = Binder.clearCallingIdentity();
try {
Intent intent;
if (specifyInitialDialpadState) {
intent = PhoneGlobals.createInCallIntent(initialDialpadState);
} else {
intent = PhoneGlobals.createInCallIntent();
}
try {
mApp.startActivity(intent);
} catch (ActivityNotFoundException e) {
// It's possible that the in-call UI might not exist
// (like on non-voice-capable devices), although we
// shouldn't be trying to bring up the InCallScreen on
// devices like that in the first place!
Log.w(LOG_TAG, "showCallScreenInternal: "
+ "transition to InCallScreen failed; intent = " + intent);
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
return true;
}
// Show the in-call screen without specifying the initial dialpad state.
public boolean showCallScreen() {
return showCallScreenInternal(false, false);
}
// The variation of showCallScreen() that specifies the initial dialpad state.
// (Ideally this would be called showCallScreen() too, just with a different
// signature, but AIDL doesn't allow that.)
public boolean showCallScreenWithDialpad(boolean showDialpad) {
return showCallScreenInternal(true, showDialpad);
}
/**
* End a call based on call state
* @return true is a call was ended
*/
public boolean endCall() {
enforceCallPermission();
return (Boolean) sendRequest(CMD_END_CALL, null);
}
public void answerRingingCall() {
if (DBG) log("answerRingingCall...");
// TODO: there should eventually be a separate "ANSWER_PHONE" permission,
// but that can probably wait till the big TelephonyManager API overhaul.
// For now, protect this call with the MODIFY_PHONE_STATE permission.
enforceModifyPermission();
sendRequestAsync(CMD_ANSWER_RINGING_CALL);
}
/**
* Make the actual telephony calls to implement answerRingingCall().
* This should only be called from the main thread of the Phone app.
* @see answerRingingCall
*
* TODO: it would be nice to return true if we answered the call, or
* false if there wasn't actually a ringing incoming call, or some
* other error occurred. (In other words, pass back the return value
* from PhoneUtils.answerCall() or PhoneUtils.answerAndEndActive().)
* But that would require calling this method via sendRequest() rather
* than sendRequestAsync(), and right now we don't actually *need* that
* return value, so let's just return void for now.
*/
private void answerRingingCallInternal() {
final boolean hasRingingCall = !mPhone.getRingingCall().isIdle();
if (hasRingingCall) {
final boolean hasActiveCall = !mPhone.getForegroundCall().isIdle();
final boolean hasHoldingCall = !mPhone.getBackgroundCall().isIdle();
if (hasActiveCall && hasHoldingCall) {
// Both lines are in use!
// TODO: provide a flag to let the caller specify what
// policy to use if both lines are in use. (The current
// behavior is hardwired to "answer incoming, end ongoing",
// which is how the CALL button is specced to behave.)
PhoneUtils.answerAndEndActive(mCM, mCM.getFirstActiveRingingCall());
return;
} else {
// answerCall() will automatically hold the current active
// call, if there is one.
PhoneUtils.answerCall(mCM.getFirstActiveRingingCall());
return;
}
} else {
// No call was ringing.
return;
}
}
public void silenceRinger() {
if (DBG) log("silenceRinger...");
// TODO: find a more appropriate permission to check here.
// (That can probably wait till the big TelephonyManager API overhaul.
// For now, protect this call with the MODIFY_PHONE_STATE permission.)
enforceModifyPermission();
sendRequestAsync(CMD_SILENCE_RINGER);
}
/**
* Internal implemenation of silenceRinger().
* This should only be called from the main thread of the Phone app.
* @see silenceRinger
*/
private void silenceRingerInternal() {
if ((mCM.getState() == PhoneConstants.State.RINGING)
&& mApp.notifier.isRinging()) {
// Ringer is actually playing, so silence it.
if (DBG) log("silenceRingerInternal: silencing...");
mApp.notifier.silenceRinger();
}
}
public boolean isOffhook() {
return (mCM.getState() == PhoneConstants.State.OFFHOOK);
}
public boolean isRinging() {
return (mCM.getState() == PhoneConstants.State.RINGING);
}
public boolean isIdle() {
return (mCM.getState() == PhoneConstants.State.IDLE);
}
public boolean isSimPinEnabled() {
enforceReadPermission();
return (PhoneGlobals.getInstance().isSimPinEnabled());
}
public boolean supplyPin(String pin) {
enforceModifyPermission();
final UnlockSim checkSimPin = new UnlockSim(mPhone.getIccCard());
checkSimPin.start();
return checkSimPin.unlockSim(null, pin);
}
public boolean supplyPuk(String puk, String pin) {
enforceModifyPermission();
final UnlockSim checkSimPuk = new UnlockSim(mPhone.getIccCard());
checkSimPuk.start();
return checkSimPuk.unlockSim(puk, pin);
}
/**
* Helper thread to turn async call to {@link SimCard#supplyPin} into
* a synchronous one.
*/
private static class UnlockSim extends Thread {
private final IccCard mSimCard;
private boolean mDone = false;
private boolean mResult = false;
// For replies from SimCard interface
private Handler mHandler;
// For async handler to identify request type
private static final int SUPPLY_PIN_COMPLETE = 100;
public UnlockSim(IccCard simCard) {
mSimCard = simCard;
}
@Override
public void run() {
Looper.prepare();
synchronized (UnlockSim.this) {
mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
AsyncResult ar = (AsyncResult) msg.obj;
switch (msg.what) {
case SUPPLY_PIN_COMPLETE:
Log.d(LOG_TAG, "SUPPLY_PIN_COMPLETE");
synchronized (UnlockSim.this) {
mResult = (ar.exception == null);
mDone = true;
UnlockSim.this.notifyAll();
}
break;
}
}
};
UnlockSim.this.notifyAll();
}
Looper.loop();
}
/*
* Use PIN or PUK to unlock SIM card
*
* If PUK is null, unlock SIM card with PIN
*
* If PUK is not null, unlock SIM card with PUK and set PIN code
*/
synchronized boolean unlockSim(String puk, String pin) {
while (mHandler == null) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
Message callback = Message.obtain(mHandler, SUPPLY_PIN_COMPLETE);
if (puk == null) {
mSimCard.supplyPin(pin, callback);
} else {
mSimCard.supplyPuk(puk, pin, callback);
}
while (!mDone) {
try {
Log.d(LOG_TAG, "wait for done");
wait();
} catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
}
Log.d(LOG_TAG, "done");
return mResult;
}
}
public void updateServiceLocation() {
// No permission check needed here: this call is harmless, and it's
// needed for the ServiceState.requestStateUpdate() call (which is
// already intentionally exposed to 3rd parties.)
mPhone.updateServiceLocation();
}
public boolean isRadioOn() {
return mPhone.getServiceState().getState() != ServiceState.STATE_POWER_OFF;
}
public void toggleRadioOnOff() {
enforceModifyPermission();
mPhone.setRadioPower(!isRadioOn());
}
public boolean setRadio(boolean turnOn) {
enforceModifyPermission();
if ((mPhone.getServiceState().getState() != ServiceState.STATE_POWER_OFF) != turnOn) {
toggleRadioOnOff();
}
return true;
}
public boolean enableDataConnectivity() {
enforceModifyPermission();
ConnectivityManager cm =
(ConnectivityManager)mApp.getSystemService(Context.CONNECTIVITY_SERVICE);
cm.setMobileDataEnabled(true);
return true;
}
public int enableApnType(String type) {
enforceModifyPermission();
return mPhone.enableApnType(type);
}
public int disableApnType(String type) {
enforceModifyPermission();
return mPhone.disableApnType(type);
}
public boolean disableDataConnectivity() {
enforceModifyPermission();
ConnectivityManager cm =
(ConnectivityManager)mApp.getSystemService(Context.CONNECTIVITY_SERVICE);
cm.setMobileDataEnabled(false);
return true;
}
public boolean isDataConnectivityPossible() {
return mPhone.isDataConnectivityPossible();
}
public boolean handlePinMmi(String dialString) {
enforceModifyPermission();
return (Boolean) sendRequest(CMD_HANDLE_PIN_MMI, dialString);
}
public void cancelMissedCallsNotification() {
enforceModifyPermission();
mApp.notificationMgr.cancelMissedCallNotification();
}
public int getCallState() {
return DefaultPhoneNotifier.convertCallState(mCM.getState());
}
public int getDataState() {
return DefaultPhoneNotifier.convertDataState(mPhone.getDataConnectionState());
}
public int getDataActivity() {
return DefaultPhoneNotifier.convertDataActivityState(mPhone.getDataActivityState());
}
@Override
public Bundle getCellLocation() {
try {
mApp.enforceCallingOrSelfPermission(
android.Manifest.permission.ACCESS_FINE_LOCATION, null);
} catch (SecurityException e) {
// If we have ACCESS_FINE_LOCATION permission, skip the check for ACCESS_COARSE_LOCATION
// A failure should throw the SecurityException from ACCESS_COARSE_LOCATION since this
// is the weaker precondition
mApp.enforceCallingOrSelfPermission(
android.Manifest.permission.ACCESS_COARSE_LOCATION, null);
}
if (checkIfCallerIsSelfOrForegoundUser()) {
if (DBG_LOC) log("getCellLocation: is active user");
Bundle data = new Bundle();
mPhone.getCellLocation().fillInNotifierBundle(data);
return data;
} else {
if (DBG_LOC) log("getCellLocation: suppress non-active user");
return null;
}
}
@Override
public void enableLocationUpdates() {
mApp.enforceCallingOrSelfPermission(
android.Manifest.permission.CONTROL_LOCATION_UPDATES, null);
mPhone.enableLocationUpdates();
}
@Override
public void disableLocationUpdates() {
mApp.enforceCallingOrSelfPermission(
android.Manifest.permission.CONTROL_LOCATION_UPDATES, null);
mPhone.disableLocationUpdates();
}
@Override
@SuppressWarnings("unchecked")
public List<NeighboringCellInfo> getNeighboringCellInfo() {
try {
mApp.enforceCallingOrSelfPermission(
android.Manifest.permission.ACCESS_FINE_LOCATION, null);
} catch (SecurityException e) {
// If we have ACCESS_FINE_LOCATION permission, skip the check
// for ACCESS_COARSE_LOCATION
// A failure should throw the SecurityException from
// ACCESS_COARSE_LOCATION since this is the weaker precondition
mApp.enforceCallingOrSelfPermission(
android.Manifest.permission.ACCESS_COARSE_LOCATION, null);
}
if (checkIfCallerIsSelfOrForegoundUser()) {
if (DBG_LOC) log("getNeighboringCellInfo: is active user");
ArrayList<NeighboringCellInfo> cells = null;
try {
cells = (ArrayList<NeighboringCellInfo>) sendRequest(
CMD_HANDLE_NEIGHBORING_CELL, null);
} catch (RuntimeException e) {
Log.e(LOG_TAG, "getNeighboringCellInfo " + e);
}
return cells;
} else {
if (DBG_LOC) log("getNeighboringCellInfo: suppress non-active user");
return null;
}
}
@Override
public List<CellInfo> getAllCellInfo() {
try {
mApp.enforceCallingOrSelfPermission(
android.Manifest.permission.ACCESS_FINE_LOCATION, null);
} catch (SecurityException e) {
// If we have ACCESS_FINE_LOCATION permission, skip the check for ACCESS_COARSE_LOCATION
// A failure should throw the SecurityException from ACCESS_COARSE_LOCATION since this
// is the weaker precondition
mApp.enforceCallingOrSelfPermission(
android.Manifest.permission.ACCESS_COARSE_LOCATION, null);
}
if (checkIfCallerIsSelfOrForegoundUser()) {
if (DBG_LOC) log("getAllCellInfo: is active user");
return mPhone.getAllCellInfo();
} else {
if (DBG_LOC) log("getAllCellInfo: suppress non-active user");
return null;
}
}
//
// Internal helper methods.
//
private boolean checkIfCallerIsSelfOrForegoundUser() {
boolean ok;
boolean self = Binder.getCallingUid() == Process.myUid();
if (!self) {
// Get the caller's user id then clear the calling identity
// which will be restored in the finally clause.
int callingUser = UserHandle.getCallingUserId();
long ident = Binder.clearCallingIdentity();
try {
// With calling identity cleared the current user is the foreground user.
int foregroundUser = ActivityManager.getCurrentUser();
ok = (foregroundUser == callingUser);
if (DBG_LOC) {
log("checkIfCallerIsSelfOrForegoundUser: foregroundUser=" + foregroundUser
+ " callingUser=" + callingUser + " ok=" + ok);
}
} catch (Exception ex) {
if (DBG_LOC) loge("checkIfCallerIsSelfOrForegoundUser: Exception ex=" + ex);
ok = false;
} finally {
Binder.restoreCallingIdentity(ident);
}
} else {
if (DBG_LOC) log("checkIfCallerIsSelfOrForegoundUser: is self");
ok = true;
}
if (DBG_LOC) log("checkIfCallerIsSelfOrForegoundUser: ret=" + ok);
return ok;
}
/**
* Make sure the caller has the READ_PHONE_STATE permission.
*
* @throws SecurityException if the caller does not have the required permission
*/
private void enforceReadPermission() {
mApp.enforceCallingOrSelfPermission(android.Manifest.permission.READ_PHONE_STATE, null);
}
/**
* Make sure the caller has the MODIFY_PHONE_STATE permission.
*
* @throws SecurityException if the caller does not have the required permission
*/
private void enforceModifyPermission() {
mApp.enforceCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE, null);
}
/**
* Make sure the caller has the CALL_PHONE permission.
*
* @throws SecurityException if the caller does not have the required permission
*/
private void enforceCallPermission() {
mApp.enforceCallingOrSelfPermission(android.Manifest.permission.CALL_PHONE, null);
}
private String createTelUrl(String number) {
if (TextUtils.isEmpty(number)) {
return null;
}
StringBuilder buf = new StringBuilder("tel:");
buf.append(number);
return buf.toString();
}
private void log(String msg) {
Log.d(LOG_TAG, "[PhoneIntfMgr] " + msg);
}
private void loge(String msg) {
Log.e(LOG_TAG, "[PhoneIntfMgr] " + msg);
}
public int getActivePhoneType() {
return mPhone.getPhoneType();
}
/**
* Returns the CDMA ERI icon index to display
*/
public int getCdmaEriIconIndex() {
return mPhone.getCdmaEriIconIndex();
}
/**
* Returns the CDMA ERI icon mode,
* 0 - ON
* 1 - FLASHING
*/
public int getCdmaEriIconMode() {
return mPhone.getCdmaEriIconMode();
}
/**
* Returns the CDMA ERI text,
*/
public String getCdmaEriText() {
return mPhone.getCdmaEriText();
}
/**
* Returns true if CDMA provisioning needs to run.
*/
public boolean needsOtaServiceProvisioning() {
return mPhone.needsOtaServiceProvisioning();
}
/**
* Returns the unread count of voicemails
*/
public int getVoiceMessageCount() {
return mPhone.getVoiceMessageCount();
}
/**
* Returns the network type
*/
public int getNetworkType() {
return mPhone.getServiceState().getNetworkType();
}
/**
* @return true if a ICC card is present
*/
public boolean hasIccCard() {
return mPhone.getIccCard().hasIccCard();
}
/**
* Return if the current radio is LTE on CDMA. This
* is a tri-state return value as for a period of time
* the mode may be unknown.
*
* @return {@link Phone#LTE_ON_CDMA_UNKNOWN}, {@link Phone#LTE_ON_CDMA_FALSE}
* or {@link PHone#LTE_ON_CDMA_TRUE}
*/
public int getLteOnCdmaMode() {
return mPhone.getLteOnCdmaMode();
}
}