diff options
author | 2020-04-09 18:26:19 -0700 | |
---|---|---|
committer | 2020-04-21 08:37:29 -0700 | |
commit | 30e7471e5d392fb2693bc825216855104b3e2d6c (patch) | |
tree | 84881bd5d5a91fe5e309a3f1dce56e052ee61a60 | |
parent | 53e927f890f08d7916ccf1ae2afeb2624561d107 (diff) |
Fix EmergencyAffordanceService
Significantly rework Emergency Affordance Service.
-Enable country-by-country behavior based on ISO not MCC.
-Rely on LocaleTracker for the cellular network country.
-Remove all locking/synchronization.
Bug: 130187110
Test: com.android.server.emergency.EmergencyAffordanceServiceTest
Test: Feature regression test located at go/EAS-regression
Change-Id: I66315988b14d7cbdb5278a1006327818dc5ab11e
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"); + } + } +} |