summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Rambo Wang <rambowang@google.com> 2020-04-22 01:18:54 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2020-04-22 01:18:54 +0000
commit87adee8718e12a230be0b757cfa78566777d2fe2 (patch)
treecd5dbd21119b2f87a53c1ecc08b9bc20571a5822
parentaffc08a3404baeeac17c514b69cba0f2c3dc5b54 (diff)
parent30e7471e5d392fb2693bc825216855104b3e2d6c (diff)
Merge "Fix EmergencyAffordanceService" into rvc-dev
-rw-r--r--core/res/res/values/config.xml7
-rw-r--r--core/res/res/values/symbols.xml2
-rw-r--r--services/core/java/com/android/server/emergency/EmergencyAffordanceService.java357
-rw-r--r--services/tests/servicestests/src/com/android/server/emergency/EmergencyAffordanceServiceTest.java486
4 files changed, 660 insertions, 192 deletions
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 1a9855311518..8f16e530dfe0 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3496,10 +3496,9 @@
<!-- Do not translate. Mcc codes whose existence trigger the presence of emergency
affordances-->
- <integer-array name="config_emergency_mcc_codes" translatable="false">
- <item>404</item>
- <item>405</item>
- </integer-array>
+ <string-array name="config_emergency_iso_country_codes" translatable="false">
+ <item>in</item>
+ </string-array>
<!-- Package name for the device provisioning package. -->
<string name="config_deviceProvisioningPackage"></string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 05c00ce51ee3..ebc3612ca1e6 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3085,7 +3085,7 @@
<java-symbol type="string" name="global_action_emergency" />
<java-symbol type="string" name="config_emergency_call_number" />
<java-symbol type="string" name="config_emergency_dialer_package" />
- <java-symbol type="array" name="config_emergency_mcc_codes" />
+ <java-symbol type="array" name="config_emergency_iso_country_codes" />
<java-symbol type="string" name="config_dozeDoubleTapSensorType" />
<java-symbol type="string" name="config_dozeTapSensorType" />
diff --git a/services/core/java/com/android/server/emergency/EmergencyAffordanceService.java b/services/core/java/com/android/server/emergency/EmergencyAffordanceService.java
index 1cf27ffd1903..cc7915cc3534 100644
--- a/services/core/java/com/android/server/emergency/EmergencyAffordanceService.java
+++ b/services/core/java/com/android/server/emergency/EmergencyAffordanceService.java
@@ -20,90 +20,80 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.os.Binder;
+import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.provider.Settings;
-import android.telephony.CellInfo;
-import android.telephony.CellInfoGsm;
-import android.telephony.CellInfoLte;
-import android.telephony.CellInfoWcdma;
-import android.telephony.CellLocation;
-import android.telephony.PhoneStateListener;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Slog;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.IndentingPrintWriter;
import com.android.server.SystemService;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
/**
- * A service that listens to connectivity and SIM card changes and determines if the emergency mode
- * should be enabled
+ * A service that listens to connectivity and SIM card changes and determines if the emergency
+ * affordance should be enabled.
*/
public class EmergencyAffordanceService extends SystemService {
private static final String TAG = "EmergencyAffordanceService";
+ private static final boolean DBG = false;
- private static final int NUM_SCANS_UNTIL_ABORT = 4;
+ private static final String SERVICE_NAME = "emergency_affordance";
private static final int INITIALIZE_STATE = 1;
- private static final int CELL_INFO_STATE_CHANGED = 2;
- private static final int SUBSCRIPTION_CHANGED = 3;
-
/**
- * Global setting, whether the last scan of the sim cards reveal that a sim was inserted that
- * requires the emergency affordance. The value is a boolean (1 or 0).
- * @hide
+ * @param arg1 slot Index
+ * @param arg2 0
+ * @param obj ISO country code
*/
- private static final String EMERGENCY_SIM_INSERTED_SETTING = "emergency_sim_inserted_before";
-
- private final Context mContext;
- private final ArrayList<Integer> mEmergencyCallMccNumbers;
+ private static final int NETWORK_COUNTRY_CHANGED = 2;
+ private static final int SUBSCRIPTION_CHANGED = 3;
+ private static final int UPDATE_AIRPLANE_MODE_STATUS = 4;
- private final Object mLock = new Object();
+ // Global Settings to override emergency affordance country ISO for debugging.
+ // Available only on debug build. The value is a country ISO string in lower case (eg. "us").
+ private static final String EMERGENCY_AFFORDANCE_OVERRIDE_ISO =
+ "emergency_affordance_override_iso";
- private TelephonyManager mTelephonyManager;
+ private final Context mContext;
+ // Country ISOs that require affordance
+ private final ArrayList<String> mEmergencyCallCountryIsos;
private SubscriptionManager mSubscriptionManager;
- private boolean mEmergencyAffordanceNeeded;
+ private TelephonyManager mTelephonyManager;
private MyHandler mHandler;
- private int mScansCompleted;
- private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
- @Override
- public void onCellInfoChanged(List<CellInfo> cellInfo) {
- if (!isEmergencyAffordanceNeeded()) {
- requestCellScan();
- }
- }
+ private boolean mAnySimNeedsEmergencyAffordance;
+ private boolean mAnyNetworkNeedsEmergencyAffordance;
+ private boolean mEmergencyAffordanceNeeded;
+ private boolean mAirplaneModeEnabled;
+ private boolean mVoiceCapable;
- @Override
- public void onCellLocationChanged(CellLocation location) {
- if (!isEmergencyAffordanceNeeded()) {
- requestCellScan();
- }
- }
- };
- private BroadcastReceiver mAirplaneModeReceiver = new BroadcastReceiver() {
+ private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- if (Settings.Global.getInt(context.getContentResolver(),
- Settings.Global.AIRPLANE_MODE_ON, 0) == 0) {
- startScanning();
- requestCellScan();
+ if (TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED.equals(intent.getAction())) {
+ String countryCode = intent.getStringExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY);
+ int slotId = intent.getIntExtra(SubscriptionManager.EXTRA_SLOT_INDEX,
+ SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+ mHandler.obtainMessage(
+ NETWORK_COUNTRY_CHANGED, slotId, 0, countryCode).sendToTarget();
+ } else if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction())) {
+ mHandler.obtainMessage(UPDATE_AIRPLANE_MODE_STATUS).sendToTarget();
}
}
};
- private boolean mSimNeedsEmergencyAffordance;
- private boolean mNetworkNeedsEmergencyAffordance;
- private boolean mVoiceCapable;
-
- private void requestCellScan() {
- mHandler.obtainMessage(CELL_INFO_STATE_CHANGED).sendToTarget();
- }
private SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionChangedListener
= new SubscriptionManager.OnSubscriptionsChangedListener() {
@@ -116,207 +106,200 @@ public class EmergencyAffordanceService extends SystemService {
public EmergencyAffordanceService(Context context) {
super(context);
mContext = context;
- int[] numbers = context.getResources().getIntArray(
- com.android.internal.R.array.config_emergency_mcc_codes);
- mEmergencyCallMccNumbers = new ArrayList<>(numbers.length);
- for (int i = 0; i < numbers.length; i++) {
- mEmergencyCallMccNumbers.add(numbers[i]);
+ String[] isos = context.getResources().getStringArray(
+ com.android.internal.R.array.config_emergency_iso_country_codes);
+ mEmergencyCallCountryIsos = new ArrayList<>(isos.length);
+ for (String iso : isos) {
+ mEmergencyCallCountryIsos.add(iso);
}
- }
- private void updateEmergencyAffordanceNeeded() {
- synchronized (mLock) {
- mEmergencyAffordanceNeeded = mVoiceCapable && (mSimNeedsEmergencyAffordance ||
- mNetworkNeedsEmergencyAffordance);
- Settings.Global.putInt(mContext.getContentResolver(),
- Settings.Global.EMERGENCY_AFFORDANCE_NEEDED,
- mEmergencyAffordanceNeeded ? 1 : 0);
- if (mEmergencyAffordanceNeeded) {
- stopScanning();
+ if (Build.IS_DEBUGGABLE) {
+ String overrideIso = Settings.Global.getString(
+ mContext.getContentResolver(), EMERGENCY_AFFORDANCE_OVERRIDE_ISO);
+ if (!TextUtils.isEmpty(overrideIso)) {
+ if (DBG) Slog.d(TAG, "Override ISO to " + overrideIso);
+ mEmergencyCallCountryIsos.clear();
+ mEmergencyCallCountryIsos.add(overrideIso);
}
}
}
- private void stopScanning() {
- synchronized (mLock) {
- mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
- mScansCompleted = 0;
- }
- }
-
- private boolean isEmergencyAffordanceNeeded() {
- synchronized (mLock) {
- return mEmergencyAffordanceNeeded;
- }
- }
-
@Override
public void onStart() {
+ if (DBG) Slog.i(TAG, "onStart");
+ publishBinderService(SERVICE_NAME, new BinderService());
}
@Override
public void onBootPhase(int phase) {
if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
- mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
- mVoiceCapable = mTelephonyManager.isVoiceCapable();
- if (!mVoiceCapable) {
- updateEmergencyAffordanceNeeded();
- return;
- }
- mSubscriptionManager = SubscriptionManager.from(mContext);
- HandlerThread thread = new HandlerThread(TAG);
- thread.start();
- mHandler = new MyHandler(thread.getLooper());
- mHandler.obtainMessage(INITIALIZE_STATE).sendToTarget();
- startScanning();
- IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
- mContext.registerReceiver(mAirplaneModeReceiver, filter);
- mSubscriptionManager.addOnSubscriptionsChangedListener(mSubscriptionChangedListener);
+ if (DBG) Slog.i(TAG, "onBootPhase");
+ handleThirdPartyBootPhase();
}
}
- private void startScanning() {
- mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CELL_INFO
- | PhoneStateListener.LISTEN_CELL_LOCATION);
- }
-
/** Handler to do the heavier work on */
private class MyHandler extends Handler {
-
public MyHandler(Looper l) {
super(l);
}
@Override
public void handleMessage(Message msg) {
+ if (DBG) Slog.d(TAG, "handleMessage: " + msg.what);
switch (msg.what) {
case INITIALIZE_STATE:
handleInitializeState();
break;
- case CELL_INFO_STATE_CHANGED:
- handleUpdateCellInfo();
+ case NETWORK_COUNTRY_CHANGED:
+ final String countryIso = (String) msg.obj;
+ final int slotId = msg.arg1;
+ handleNetworkCountryChanged(countryIso, slotId);
break;
case SUBSCRIPTION_CHANGED:
handleUpdateSimSubscriptionInfo();
break;
+ case UPDATE_AIRPLANE_MODE_STATUS:
+ handleUpdateAirplaneModeStatus();
+ break;
+ default:
+ Slog.e(TAG, "Unexpected message received: " + msg.what);
}
}
}
private void handleInitializeState() {
- if (handleUpdateSimSubscriptionInfo()) {
- return;
- }
- if (handleUpdateCellInfo()) {
+ if (DBG) Slog.d(TAG, "handleInitializeState");
+ handleUpdateAirplaneModeStatus();
+ handleUpdateSimSubscriptionInfo();
+ updateNetworkCountry();
+ updateEmergencyAffordanceNeeded();
+ }
+
+ private void handleThirdPartyBootPhase() {
+ if (DBG) Slog.d(TAG, "handleThirdPartyBootPhase");
+ mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
+ mVoiceCapable = mTelephonyManager.isVoiceCapable();
+ if (!mVoiceCapable) {
+ updateEmergencyAffordanceNeeded();
return;
}
- updateEmergencyAffordanceNeeded();
+
+ HandlerThread thread = new HandlerThread(TAG);
+ thread.start();
+ mHandler = new MyHandler(thread.getLooper());
+
+ mSubscriptionManager = SubscriptionManager.from(mContext);
+ mSubscriptionManager.addOnSubscriptionsChangedListener(mSubscriptionChangedListener);
+
+ IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ filter.addAction(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED);
+ mContext.registerReceiver(mBroadcastReceiver, filter);
+
+ mHandler.obtainMessage(INITIALIZE_STATE).sendToTarget();
+ }
+
+ private void handleUpdateAirplaneModeStatus() {
+ mAirplaneModeEnabled = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
+ if (DBG) Slog.d(TAG, "APM status updated to " + mAirplaneModeEnabled);
}
- private boolean handleUpdateSimSubscriptionInfo() {
- boolean neededBefore = simNeededAffordanceBefore();
- boolean neededNow = neededBefore;
+ private void handleUpdateSimSubscriptionInfo() {
List<SubscriptionInfo> activeSubscriptionInfoList =
mSubscriptionManager.getActiveSubscriptionInfoList();
+ if (DBG) Slog.d(TAG, "handleUpdateSimSubscriptionInfo: " + activeSubscriptionInfoList);
if (activeSubscriptionInfoList == null) {
- setSimNeedsEmergencyAffordance(neededNow);
- return neededNow;
+ return;
}
+
+ boolean needsAffordance = false;
for (SubscriptionInfo info : activeSubscriptionInfoList) {
- int mcc = info.getMcc();
- if (mccRequiresEmergencyAffordance(mcc)) {
- neededNow = true;
+ if (isoRequiresEmergencyAffordance(info.getCountryIso())) {
+ needsAffordance = true;
break;
- } else if (mcc != 0 && mcc != Integer.MAX_VALUE){
- // a Sim with a different mcc code was found
- neededNow = false;
- }
- String simOperator = mTelephonyManager
- .createForSubscriptionId(info.getSubscriptionId()).getSimOperator();
- mcc = 0;
- if (simOperator != null && simOperator.length() >= 3) {
- mcc = Integer.parseInt(simOperator.substring(0, 3));
- }
- if (mcc != 0) {
- if (mccRequiresEmergencyAffordance(mcc)) {
- neededNow = true;
- break;
- } else {
- // a Sim with a different mcc code was found
- neededNow = false;
- }
}
}
- setSimNeedsEmergencyAffordance(neededNow);
- return neededNow;
+
+ mAnySimNeedsEmergencyAffordance = needsAffordance;
+ updateEmergencyAffordanceNeeded();
}
- private void setSimNeedsEmergencyAffordance(boolean simNeedsEmergencyAffordance) {
- if (simNeededAffordanceBefore() != simNeedsEmergencyAffordance) {
- Settings.Global.putInt(mContext.getContentResolver(),
- EMERGENCY_SIM_INSERTED_SETTING,
- simNeedsEmergencyAffordance ? 1 : 0);
+ private void handleNetworkCountryChanged(String countryIso, int slotId) {
+ if (DBG) {
+ Slog.d(TAG, "handleNetworkCountryChanged: countryIso=" + countryIso
+ + ", slotId=" + slotId);
}
- if (simNeedsEmergencyAffordance != mSimNeedsEmergencyAffordance) {
- mSimNeedsEmergencyAffordance = simNeedsEmergencyAffordance;
- updateEmergencyAffordanceNeeded();
+
+ if (TextUtils.isEmpty(countryIso) && mAirplaneModeEnabled) {
+ Slog.w(TAG, "Ignore empty countryIso report when APM is on.");
+ return;
}
- }
- private boolean simNeededAffordanceBefore() {
- return Settings.Global.getInt(mContext.getContentResolver(),
- EMERGENCY_SIM_INSERTED_SETTING, 0) != 0;
+ updateNetworkCountry();
+
+ updateEmergencyAffordanceNeeded();
}
- private boolean handleUpdateCellInfo() {
- List<CellInfo> cellInfos = mTelephonyManager.getAllCellInfo();
- if (cellInfos == null) {
- return false;
- }
- boolean stopScanningAfterScan = false;
- for (CellInfo cellInfo : cellInfos) {
- int mcc = 0;
- if (cellInfo instanceof CellInfoGsm) {
- mcc = ((CellInfoGsm) cellInfo).getCellIdentity().getMcc();
- } else if (cellInfo instanceof CellInfoLte) {
- mcc = ((CellInfoLte) cellInfo).getCellIdentity().getMcc();
- } else if (cellInfo instanceof CellInfoWcdma) {
- mcc = ((CellInfoWcdma) cellInfo).getCellIdentity().getMcc();
- }
- if (mccRequiresEmergencyAffordance(mcc)) {
- setNetworkNeedsEmergencyAffordance(true);
- return true;
- } else if (mcc != 0 && mcc != Integer.MAX_VALUE) {
- // we found an mcc that isn't in the list, abort
- stopScanningAfterScan = true;
+ private void updateNetworkCountry() {
+ boolean needsAffordance = false;
+
+ final int activeModems = mTelephonyManager.getActiveModemCount();
+ for (int i = 0; i < activeModems; i++) {
+ String countryIso = mTelephonyManager.getNetworkCountryIso(i);
+ if (DBG) Slog.d(TAG, "UpdateNetworkCountry: slotId=" + i + " countryIso=" + countryIso);
+ if (isoRequiresEmergencyAffordance(countryIso)) {
+ needsAffordance = true;
+ break;
}
}
- if (stopScanningAfterScan) {
- stopScanning();
- } else {
- onCellScanFinishedUnsuccessful();
- }
- setNetworkNeedsEmergencyAffordance(false);
- return false;
+
+ mAnyNetworkNeedsEmergencyAffordance = needsAffordance;
+
+ updateEmergencyAffordanceNeeded();
}
- private void setNetworkNeedsEmergencyAffordance(boolean needsAffordance) {
- synchronized (mLock) {
- mNetworkNeedsEmergencyAffordance = needsAffordance;
- updateEmergencyAffordanceNeeded();
- }
+ private boolean isoRequiresEmergencyAffordance(String iso) {
+ return mEmergencyCallCountryIsos.contains(iso);
}
- private void onCellScanFinishedUnsuccessful() {
- synchronized (mLock) {
- mScansCompleted++;
- if (mScansCompleted >= NUM_SCANS_UNTIL_ABORT) {
- stopScanning();
- }
+ private void updateEmergencyAffordanceNeeded() {
+ if (DBG) {
+ Slog.d(TAG, "updateEmergencyAffordanceNeeded: mEmergencyAffordanceNeeded="
+ + mEmergencyAffordanceNeeded + ", mVoiceCapable=" + mVoiceCapable
+ + ", mAnySimNeedsEmergencyAffordance=" + mAnySimNeedsEmergencyAffordance
+ + ", mAnyNetworkNeedsEmergencyAffordance="
+ + mAnyNetworkNeedsEmergencyAffordance);
+ }
+ boolean lastAffordanceNeeded = mEmergencyAffordanceNeeded;
+
+ mEmergencyAffordanceNeeded = mVoiceCapable
+ && (mAnySimNeedsEmergencyAffordance || mAnyNetworkNeedsEmergencyAffordance);
+
+ if (lastAffordanceNeeded != mEmergencyAffordanceNeeded) {
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.EMERGENCY_AFFORDANCE_NEEDED,
+ mEmergencyAffordanceNeeded ? 1 : 0);
}
}
- private boolean mccRequiresEmergencyAffordance(int mcc) {
- return mEmergencyCallMccNumbers.contains(mcc);
+ private void dumpInternal(IndentingPrintWriter ipw) {
+ ipw.println("EmergencyAffordanceService (dumpsys emergency_affordance) state:\n");
+ ipw.println("mEmergencyAffordanceNeeded=" + mEmergencyAffordanceNeeded);
+ ipw.println("mVoiceCapable=" + mVoiceCapable);
+ ipw.println("mAnySimNeedsEmergencyAffordance=" + mAnySimNeedsEmergencyAffordance);
+ ipw.println("mAnyNetworkNeedsEmergencyAffordance=" + mAnyNetworkNeedsEmergencyAffordance);
+ ipw.println("mEmergencyCallCountryIsos=" + String.join(",", mEmergencyCallCountryIsos));
+ }
+
+ private final class BinderService extends Binder {
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) {
+ return;
+ }
+
+ dumpInternal(new IndentingPrintWriter(pw, " "));
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/emergency/EmergencyAffordanceServiceTest.java b/services/tests/servicestests/src/com/android/server/emergency/EmergencyAffordanceServiceTest.java
new file mode 100644
index 000000000000..d438a0eb9411
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/emergency/EmergencyAffordanceServiceTest.java
@@ -0,0 +1,486 @@
+/*
+ * Copyright (C) 2020 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.server.emergency;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import android.telephony.TelephonyManager;
+import android.test.mock.MockContentResolver;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.server.SystemService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * Unit test for EmergencyAffordanceService (EAS for short) which determines when
+ * should we enable Emergency Affordance feature (EA for short).
+ *
+ * Please refer to https://source.android.com/devices/tech/connect/emergency-affordance
+ * to see the details of the feature.
+ */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class EmergencyAffordanceServiceTest {
+
+ // Default country ISO that should enable EA. Value comes from resource
+ // com.android.internal.R.array.config_emergency_iso_country_codes
+ private static final String EMERGENCY_ISO_CODE = "in";
+ // Randomly picked country ISO that should not enable EA.
+ private static final String NON_EMERGENCY_ISO_CODE = "us";
+
+ // Valid values for Settings.Global.EMERGENCY_AFFORDANCE_NEEDED
+ private static final int OFF = 0; // which means feature disabled
+ private static final int ON = 1; // which means feature enabled
+
+ private static final int ACTIVE_MODEM_COUNT = 2;
+
+ @Mock private Resources mResources;
+ @Mock private SubscriptionManager mSubscriptionManager;
+ @Mock private TelephonyManager mTelephonyManager;
+
+ private TestContext mServiceContext;
+ private MockContentResolver mContentResolver;
+ private OnSubscriptionsChangedListener mSubscriptionChangedListener;
+ private EmergencyAffordanceService mService;
+
+ // Testable Context that mocks resources, content resolver and system services
+ private class TestContext extends BroadcastInterceptingContext {
+ TestContext(Context base) {
+ super(base);
+ }
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return mContentResolver;
+ }
+
+ @Override
+ public Resources getResources() {
+ return mResources;
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ switch (name) {
+ case Context.TELEPHONY_SUBSCRIPTION_SERVICE:
+ return mSubscriptionManager;
+ case Context.TELEPHONY_SERVICE:
+ return mTelephonyManager;
+ default:
+ return super.getSystemService(name);
+ }
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ doReturn(new String[] { EMERGENCY_ISO_CODE }).when(mResources)
+ .getStringArray(com.android.internal.R.array.config_emergency_iso_country_codes);
+
+ final Context context = InstrumentationRegistry.getContext();
+ mServiceContext = new TestContext(context);
+ mContentResolver = new MockContentResolver(mServiceContext);
+ mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+
+ // Initialize feature off, to have constant starting
+ Settings.Global.putInt(mContentResolver, Settings.Global.EMERGENCY_AFFORDANCE_NEEDED, 0);
+ mService = new EmergencyAffordanceService(mServiceContext);
+ }
+
+ /**
+ * Verify if the device is not voice capable, the feature should be disabled.
+ */
+ @Test
+ public void testSettings_shouldBeOff_whenVoiceCapableIsFalse() throws Exception {
+ // Given: the device is not voice capable
+ // When: setup device and boot service
+ setUpDevice(false /* withVoiceCapable */, true /* withEmergencyIsoInSim */,
+ true /* withEmergencyIsoInCell */);
+
+ // Then: EA setting will should be 0
+ verifyEmergencyAffordanceNeededSettings(OFF);
+ }
+
+ /**
+ * Verify the voice capable device is booted up without EA-enabled cell network, with
+ * no EA-enabled SIM installed, feature should be disabled.
+ */
+ @Test
+ public void testSettings_shouldBeOff_whenWithoutEAEanbledNetworkNorSim() throws Exception {
+ // Given: the device is voice capble, no EA-enable SIM, no EA-enabled Cell
+ setUpDevice(true /* withVoiceCapable */, false /* withEmergencyIsoInSim */,
+ false /* withEmergencyIsoInCell */);
+
+ // Then: EA setting will should be 0
+ verifyEmergencyAffordanceNeededSettings(OFF);
+ }
+
+ /**
+ * Verify the voice capable device is booted up with EA-enabled SIM installed, the
+ * feature should be enabled.
+ */
+ @Test
+ public void testSettings_shouldBeOn_whenBootUpWithEAEanbledSim() throws Exception {
+ // Given: the device is voice capble, with EA-enable SIM, no EA-enabled Cell
+ setUpDevice(true /* withVoiceCapable */, true /* withEmergencyIsoInSim */,
+ false /* withEmergencyIsoInCell */);
+
+ // Then: EA setting will immediately update to 1
+ verifyEmergencyAffordanceNeededSettings(ON);
+ }
+
+ /**
+ * Verify the voice capable device is booted up with EA-enabled Cell network, the
+ * feature should be enabled.
+ */
+ @Test
+ public void testSettings_shouldBeOn_whenBootUpWithEAEanbledCell() throws Exception {
+ // Given: the device is voice capble, with EA-enable SIM, with EA-enabled Cell
+ setUpDevice(true /* withVoiceCapable */, false /* withEmergencyIsoInSim */,
+ true /* withEmergencyIsoInCell */);
+
+ // Then: EA setting will immediately update to 1
+ verifyEmergencyAffordanceNeededSettings(ON);
+ }
+
+ /**
+ * Verify when device boot up with no EA-enabled SIM, but later install one,
+ * feature should be enabled.
+ */
+ @Test
+ public void testSettings_shouldBeOn_whenSubscriptionInfoChangedWithEmergencyIso()
+ throws Exception {
+ // Given: the device is voice capable, boot up with no EA-enabled SIM, no EA-enabled Cell
+ setUpDevice(true /* withVoiceCapable */, false/* withEmergencyIsoInSim */,
+ false /* withEmergencyIsoInCell */);
+
+ // When: Insert EA-enabled SIM and get notified
+ setUpSim(true /* withEmergencyIsoInSim */);
+ mSubscriptionChangedListener.onSubscriptionsChanged();
+
+ // Then: EA Setting will update to 1
+ verifyEmergencyAffordanceNeededSettings(ON);
+ }
+
+ /**
+ * Verify when feature was on, device re-insert with no EA-enabled SIMs,
+ * feature should be disabled.
+ */
+ @Test
+ public void testSettings_shouldBeOff_whenSubscriptionInfoChangedWithoutEmergencyIso()
+ throws Exception {
+ // Given: the device is voice capable, no EA-enabled Cell, with EA-enabled SIM
+ setUpDevice(true /* withVoiceCapable */, true /* withEmergencyIsoInSim */,
+ false /* withEmergencyIsoInCell */);
+
+ // When: All SIMs are replaced with EA-disabled ones.
+ setUpSim(false /* withEmergencyIsoInSim */);
+ mSubscriptionChangedListener.onSubscriptionsChanged();
+
+ // Then: EA Setting will update to 0
+ verifyEmergencyAffordanceNeededSettings(OFF);
+ }
+
+ /**
+ * Verify when device boot up with no EA-enabled Cell, but later move into one,
+ * feature should be enabled.
+ */
+ @Test
+ public void testSettings_shouldBeOn_whenCountryIsoChangedWithEmergencyIso()
+ throws Exception {
+ // Given: the device is voice capable, boot up with no EA-enabled SIM, no EA-enabled Cell
+ setUpDevice(true /* withVoiceCapable */, false/* withEmergencyIsoInSim */,
+ false /* withEmergencyIsoInCell */);
+
+ // When: device locale change to EA-enabled Cell and get notified
+ resetCell(true /* withEmergencyIsoInSim */);
+ sendBroadcastNetworkCountryChanged(EMERGENCY_COUNTRY_ISO);
+
+ // Then: EA Setting will update to 1
+ verifyEmergencyAffordanceNeededSettings(ON);
+ }
+
+ /**
+ * Verify when device boot up with EA-enabled Cell, but later move out of it,
+ * feature should be enabled.
+ */
+ @Test
+ public void testSettings_shouldBeOff_whenCountryIsoChangedWithoutEmergencyIso()
+ throws Exception {
+ // Given: the device is voice capable, boot up with no EA-enabled SIM, with EA-enabled Cell
+ setUpDevice(true /* withVoiceCapable */, false/* withEmergencyIsoInSim */,
+ true /* withEmergencyIsoInCell */);
+
+ // When: device locale change to no EA-enabled Cell and get notified
+ resetCell(false /* withEmergencyIsoInSim */);
+ sendBroadcastNetworkCountryChanged(NON_EMERGENCY_COUNTRY_ISO);
+
+ // Then: EA Setting will update to 0
+ verifyEmergencyAffordanceNeededSettings(OFF);
+ }
+ /**
+ * Verify if device is not in EA-enabled Mobile Network without EA-enable SIM(s) installed,
+ * when receive SubscriptionInfo change, the feature should not be enabled.
+ */
+ @Test
+ public void testSettings_shouldBeOff_whenNoEmergencyIsoInCellNorSim() throws Exception {
+ // Given: the device is voice capable, no EA-enabled Cell, no EA-enabled SIM
+ setUpDevice(true /* withVoiceCapable */, false /* withEmergencyIsoInSim */,
+ false /* withEmergencyIsoInCell */);
+
+ // When: Subscription changed event received
+ mSubscriptionChangedListener.onSubscriptionsChanged();
+
+ // Then: EA Settings should be 0
+ verifyEmergencyAffordanceNeededSettings(OFF);
+ }
+
+ /**
+ * Verify while feature was on, when device receive empty country iso change, while APM is
+ * enabled, feature status should keep on.
+ */
+ @Test
+ public void testSettings_shouldOn_whenReceiveEmptyISOWithAPMEnabled() throws Exception {
+ // Given: the device is voice capable, no EA-enabled SIM, with EA-enabled Cell
+ setUpDevice(true /* withVoiceCapable */, false /* withEmergencyIsoInSim */,
+ true /* withEmergencyIsoInCell */);
+
+ // Then: EA Settings will update to 1
+ verifyEmergencyAffordanceNeededSettings(ON);
+
+ // When: Airplane mode is enabled, and we receive EMPTY ISO in locale change
+ setAirplaneMode(true);
+ sendBroadcastNetworkCountryChanged(EMPTY_COUNTRY_ISO);
+
+ // Then: EA Settings will keep to 1
+ verifyEmergencyAffordanceNeededSettings(ON);
+ }
+
+ /**
+ * Verify while feature was on, when device receive empty country iso change, while APM is
+ * disabled, feature should be disabled.
+ */
+ @Test
+ public void testSettings_shouldOff_whenReceiveEmptyISOWithAPMDisabled() throws Exception {
+ // Given: the device is voice capable, no EA-enabled SIM, with EA-enabled Cell
+ setUpDevice(true /* withVoiceCapable */, false /* withEmergencyIsoInSim */,
+ true /* withEmergencyIsoInCell */);
+
+ // Then: EA Settings will update to 1
+ verifyEmergencyAffordanceNeededSettings(ON);
+
+ // When: Airplane mode is disabled, and we receive valid empty ISO in locale change
+ setUpCell(false /* withEmergencyIsoInCell */);
+ setAirplaneMode(false);
+ sendBroadcastNetworkCountryChanged(EMPTY_COUNTRY_ISO);
+
+ // Then: EA Settings will keep to 0
+ verifyEmergencyAffordanceNeededSettings(OFF);
+ }
+
+ /**
+ * Verify when airplane mode is turn on and off in cell network with EA-enabled ISO,
+ * feature should keep enabled.
+ */
+ @Test
+ public void testSettings_shouldBeOn_whenAirplaneModeOnOffWithEmergencyIsoInCell()
+ throws Exception {
+ // Given: the device is voice capable, no EA-enabled SIM, with EA-enabled Cell
+ setUpDevice(true /* withVoiceCapable */, false /* withEmergencyIsoInSim */,
+ true /* withEmergencyIsoInCell */);
+
+ // When: Device receive locale change with EA-enabled iso
+ sendBroadcastNetworkCountryChanged(EMERGENCY_COUNTRY_ISO);
+
+ // When: Airplane mode is disabled
+ setAirplaneMode(false);
+
+ // Then: EA Settings will keep with 1
+ verifyEmergencyAffordanceNeededSettings(ON);
+
+ // When: Airplane mode is enabled
+ setAirplaneMode(true);
+
+ // Then: EA Settings is still 1
+ verifyEmergencyAffordanceNeededSettings(ON);
+ }
+
+ /**
+ * Verify when airplane mode is turn on and off with EA-enabled ISO in SIM,
+ * feature should keep enabled.
+ */
+ @Test
+ public void testSettings_shouldBeOn_whenAirplaneModeOnOffWithEmergencyIsoInSim()
+ throws Exception {
+ // Given: the device is voice capable, no EA-enabled Cell Network, with EA-enabled SIM
+ setUpDevice(true /* withVoiceCapable */, true /* withEmergencyIsoInSim */,
+ false /* withEmergencyIsoInCell */);
+
+ // When: Airplane mode is disabled
+ setAirplaneMode(false);
+
+ // Then: EA Settings will keep with 1
+ verifyEmergencyAffordanceNeededSettings(ON);
+
+ // When: Airplane mode is enabled
+ setAirplaneMode(true);
+
+ // Then: EA Settings is still 1
+ verifyEmergencyAffordanceNeededSettings(ON);
+ }
+
+ // EAS reads voice capable during boot up and cache it. To test non voice capable device,
+ // we can not put this in setUp
+ private void setUpDevice(boolean withVoiceCapable, boolean withEmergencyIsoInSim,
+ boolean withEmergencyIsoInCell) throws Exception {
+ setUpVoiceCapable(withVoiceCapable);
+
+ setUpSim(withEmergencyIsoInSim);
+
+ setUpCell(withEmergencyIsoInCell);
+
+ // bypass onStart which is used to publish binder service and need sepolicy policy update
+ // mService.onStart();
+
+ mService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+
+ if (!withVoiceCapable) {
+ return;
+ }
+
+ captureSubscriptionChangeListener();
+ }
+
+ private void setUpVoiceCapable(boolean voiceCapable) {
+ doReturn(voiceCapable).when(mTelephonyManager).isVoiceCapable();
+ }
+
+ private static final Supplier<String> EMPTY_COUNTRY_ISO = () -> "";
+ private static final Supplier<String> EMERGENCY_COUNTRY_ISO = () -> EMERGENCY_ISO_CODE;
+ private static final Supplier<String> NON_EMERGENCY_COUNTRY_ISO = () -> NON_EMERGENCY_ISO_CODE;
+ private void sendBroadcastNetworkCountryChanged(Supplier<String> countryIso) {
+ Intent intent = new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED);
+ intent.putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, countryIso.get());
+ SubscriptionManager.putPhoneIdAndSubIdExtra(intent, 0);
+ mServiceContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ private void setUpSim(boolean withEmergencyIsoInSim) {
+ List<SubscriptionInfo> subInfos = getSubscriptionInfoList(withEmergencyIsoInSim);
+ doReturn(subInfos).when(mSubscriptionManager).getActiveSubscriptionInfoList();
+ }
+
+ private void setUpCell(boolean withEmergencyIsoInCell) {
+ doReturn(ACTIVE_MODEM_COUNT).when(mTelephonyManager).getActiveModemCount();
+ doReturn(NON_EMERGENCY_ISO_CODE).when(mTelephonyManager).getNetworkCountryIso(0);
+ doReturn(withEmergencyIsoInCell ? EMERGENCY_ISO_CODE : NON_EMERGENCY_ISO_CODE)
+ .when(mTelephonyManager).getNetworkCountryIso(1);
+ }
+
+ private void resetCell(boolean withEmergencyIsoInCell) {
+ doReturn(withEmergencyIsoInCell ? EMERGENCY_ISO_CODE : NON_EMERGENCY_ISO_CODE)
+ .when(mTelephonyManager).getNetworkCountryIso(1);
+ }
+
+ private void captureSubscriptionChangeListener() {
+ final ArgumentCaptor<OnSubscriptionsChangedListener> subChangedListenerCaptor =
+ ArgumentCaptor.forClass(OnSubscriptionsChangedListener.class);
+ verify(mSubscriptionManager).addOnSubscriptionsChangedListener(
+ subChangedListenerCaptor.capture());
+ mSubscriptionChangedListener = subChangedListenerCaptor.getValue();
+ }
+
+ private void setAirplaneMode(boolean enabled) {
+ // Change the system settings
+ Settings.Global.putInt(mContentResolver, Settings.Global.AIRPLANE_MODE_ON,
+ enabled ? 1 : 0);
+
+ // Post the intent
+ final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ intent.putExtra("state", enabled);
+ mServiceContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ private List<SubscriptionInfo> getSubscriptionInfoList(boolean withEmergencyIso) {
+ List<SubscriptionInfo> subInfos = new ArrayList<>(2);
+
+ // Test with Multiple SIMs. SIM1 is a non-EA SIM
+ // Only country iso is valuable, all other info are filled with dummy values
+ SubscriptionInfo subInfo = new SubscriptionInfo(1, "890126042XXXXXXXXXXX", 0, "T-mobile",
+ "T-mobile", 0, 255, "12345", 0, null,
+ "310", "226", NON_EMERGENCY_ISO_CODE, false, null, null);
+ subInfos.add(subInfo);
+
+ // SIM2 can configured to be non-EA or EA SIM according parameter withEmergencyIso
+ SubscriptionInfo subInfo2 = new SubscriptionInfo(1, "890126042XXXXXXXXXXX", 0, "Airtel",
+ "Aritel", 0, 255, "12345", 0, null, "310", "226",
+ withEmergencyIso ? EMERGENCY_ISO_CODE : NON_EMERGENCY_ISO_CODE,
+ false, null, null);
+ subInfos.add(subInfo2);
+
+ return subInfos;
+ }
+
+ // EAS has handler thread to perform heavy work, while FakeSettingProvider does not support
+ // ContentObserver. To make sure consistent result, we use a simple sleep & retry to wait for
+ // real work finished before verify result.
+ private static final int TIME_DELAY_BEFORE_VERIFY_IN_MS = 50;
+ private static final int RETRIES_BEFORE_VERIFY = 20;
+ private void verifyEmergencyAffordanceNeededSettings(int expected) throws Exception {
+ try {
+ int ct = 0;
+ int actual = -1;
+ while (ct++ < RETRIES_BEFORE_VERIFY
+ && (actual = Settings.Global.getInt(mContentResolver,
+ Settings.Global.EMERGENCY_AFFORDANCE_NEEDED)) != expected) {
+ Thread.sleep(TIME_DELAY_BEFORE_VERIFY_IN_MS);
+ }
+ assertEquals(expected, actual);
+ } catch (Settings.SettingNotFoundException e) {
+ fail("SettingNotFoundException thrown for Settings.Global.EMERGENCY_AFFORDANCE_NEEDED");
+ }
+ }
+}