| /* |
| * Copyright (C) 2022 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.sdksandbox; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.junit.Assert.assertThrows; |
| |
| import android.Manifest; |
| import android.app.ActivityManager; |
| import android.app.sdksandbox.ILoadSdkCallback; |
| import android.app.sdksandbox.ISendDataCallback; |
| import android.app.sdksandbox.SandboxedSdkContext; |
| import android.app.sdksandbox.SdkSandboxManager; |
| import android.app.sdksandbox.testutils.FakeLoadSdkCallbackBinder; |
| import android.app.sdksandbox.testutils.FakeRequestSurfacePackageCallbackBinder; |
| import android.app.sdksandbox.testutils.FakeSdkSandboxLifecycleCallbackBinder; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageManager; |
| import android.content.res.AssetManager; |
| import android.content.res.Resources; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.Process; |
| import android.os.RemoteException; |
| import android.util.ArrayMap; |
| |
| import androidx.annotation.Nullable; |
| import androidx.test.platform.app.InstrumentationRegistry; |
| |
| import com.android.dx.mockito.inline.extended.ExtendedMockito; |
| import com.android.sdksandbox.IDataReceivedCallback; |
| import com.android.sdksandbox.ILoadSdkInSandboxCallback; |
| import com.android.sdksandbox.IRequestSurfacePackageFromSdkCallback; |
| import com.android.sdksandbox.ISdkSandboxManagerToSdkSandboxCallback; |
| import com.android.sdksandbox.ISdkSandboxService; |
| import com.android.server.LocalManagerRegistry; |
| import com.android.server.pm.PackageManagerLocal; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.ArgumentMatchers; |
| import org.mockito.Mockito; |
| import org.mockito.MockitoSession; |
| |
| import java.io.BufferedReader; |
| import java.io.FileDescriptor; |
| import java.io.InputStreamReader; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.util.Objects; |
| |
| /** |
| * Unit tests for {@link SdkSandboxManagerService}. |
| */ |
| public class SdkSandboxManagerServiceUnitTest { |
| |
| private SdkSandboxManagerService mService; |
| private ActivityManager mAmSpy; |
| private FakeSdkSandboxService mSdkSandboxService; |
| private FakeSdkSandboxProvider mProvider; |
| private MockitoSession mStaticMockSession = null; |
| private Context mSpyContext; |
| |
| private static final String CLIENT_PACKAGE_NAME = "com.android.client"; |
| private static final String SDK_NAME = "com.android.codeprovider"; |
| private static final String SDK_PROVIDER_PACKAGE = "com.android.codeprovider_1"; |
| private static final String SDK_PROVIDER_RESOURCES_SDK_NAME = |
| "com.android.codeproviderresources"; |
| private static final String SDK_PROVIDER_RESOURCES_PACKAGE = |
| "com.android.codeproviderresources_1"; |
| private static final String TEST_PACKAGE = "com.android.server.sdksandbox.tests"; |
| |
| @Before |
| public void setup() { |
| mStaticMockSession = ExtendedMockito.mockitoSession() |
| .mockStatic(LocalManagerRegistry.class) |
| .startMocking(); |
| |
| Context context = InstrumentationRegistry.getInstrumentation().getContext(); |
| mSpyContext = Mockito.spy(context); |
| |
| ActivityManager am = context.getSystemService(ActivityManager.class); |
| mAmSpy = Mockito.spy(Objects.requireNonNull(am)); |
| |
| Mockito.when(mSpyContext.getSystemService(ActivityManager.class)).thenReturn(mAmSpy); |
| |
| // Required to access <sdk-library> information. |
| InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( |
| Manifest.permission.ACCESS_SHARED_LIBRARIES, Manifest.permission.INSTALL_PACKAGES); |
| mSdkSandboxService = Mockito.spy(FakeSdkSandboxService.class); |
| mProvider = new FakeSdkSandboxProvider(mSdkSandboxService); |
| |
| // Populate LocalManagerRegistry |
| ExtendedMockito.doReturn(Mockito.mock(PackageManagerLocal.class)) |
| .when(() -> LocalManagerRegistry.getManager(PackageManagerLocal.class)); |
| |
| mService = new SdkSandboxManagerService(mSpyContext, mProvider); |
| } |
| |
| @After |
| public void tearDown() { |
| mStaticMockSession.finishMocking(); |
| } |
| |
| /** Mock the ActivityManager::killUid to avoid SecurityException thrown in test. **/ |
| private void disableKillUid() { |
| Mockito.doNothing().when(mAmSpy).killUid(Mockito.anyInt(), Mockito.anyString()); |
| } |
| |
| /* Ignores network permission checks. */ |
| private void disableNetworkPermissionChecks() { |
| Mockito.doNothing().when(mSpyContext).enforceCallingPermission( |
| Mockito.eq("android.permission.INTERNET"), Mockito.anyString()); |
| Mockito.doNothing().when(mSpyContext).enforceCallingPermission( |
| Mockito.eq("android.permission.ACCESS_NETWORK_STATE"), Mockito.anyString()); |
| } |
| |
| @Test |
| public void testLoadSdkIsSuccessful() throws Exception { |
| disableNetworkPermissionChecks(); |
| |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| // Assume SupplementalProcess loads successfully |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback.isLoadSdkSuccessful()).isTrue(); |
| } |
| |
| @Test |
| public void testLoadSdkNonExistentCallingPackage() { |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| SecurityException thrown = assertThrows( |
| SecurityException.class, |
| () -> mService.loadSdk("does.not.exist", SDK_NAME, new Bundle(), |
| callback) |
| ); |
| assertThat(thrown).hasMessageThat().contains("does.not.exist not found"); |
| } |
| |
| @Test |
| public void testLoadSdkIncorrectCallingPackage() { |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| SecurityException thrown = assertThrows( |
| SecurityException.class, |
| () -> mService.loadSdk(SDK_PROVIDER_PACKAGE, SDK_NAME, new Bundle(), |
| callback) |
| ); |
| assertThat(thrown).hasMessageThat().contains("does not belong to uid"); |
| } |
| |
| @Test |
| public void testLoadSdkPackageDoesNotExist() { |
| disableNetworkPermissionChecks(); |
| |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, "does.not.exist", new Bundle(), callback); |
| |
| // Verify loading failed |
| assertThat(callback.isLoadSdkSuccessful()).isFalse(); |
| assertThat(callback.getLoadSdkErrorCode()) |
| .isEqualTo(SdkSandboxManager.LOAD_SDK_NOT_FOUND); |
| assertThat(callback.getLoadSdkErrorMsg()).contains("not found for loading"); |
| } |
| |
| @Test |
| public void testLoadSdk_errorFromSdkSandbox() throws Exception { |
| disableNetworkPermissionChecks(); |
| |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| mSdkSandboxService.sendLoadCodeError(); |
| |
| // Verify loading failed |
| assertThat(callback.isLoadSdkSuccessful()).isFalse(); |
| assertThat(callback.getLoadSdkErrorCode()).isEqualTo( |
| SdkSandboxManager.LOAD_SDK_INTERNAL_ERROR); |
| } |
| |
| @Test |
| public void testLoadSdk_errorNoInternet() throws Exception { |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| SecurityException thrown = assertThrows(SecurityException.class, |
| () -> mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback)); |
| |
| assertThat(thrown).hasMessageThat().contains(android.Manifest.permission.INTERNET); |
| } |
| |
| @Test |
| public void testLoadSdk_errorNoAccessNetworkState() throws Exception { |
| // Stub out internet permission check |
| Mockito.doNothing().when(mSpyContext).enforceCallingPermission( |
| Mockito.eq("android.permission.INTERNET"), Mockito.anyString()); |
| |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| SecurityException thrown = assertThrows(SecurityException.class, |
| () -> mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback)); |
| |
| assertThat(thrown).hasMessageThat().contains( |
| android.Manifest.permission.ACCESS_NETWORK_STATE); |
| } |
| |
| |
| @Test |
| public void testLoadSdk_successOnFirstLoad_errorOnLoadAgain() throws Exception { |
| disableNetworkPermissionChecks(); |
| |
| // Load it once |
| { |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| // Assume SupplementalProcess loads successfully |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback.isLoadSdkSuccessful()).isTrue(); |
| } |
| |
| // Load it again |
| { |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| // Verify loading failed |
| assertThat(callback.isLoadSdkSuccessful()).isFalse(); |
| assertThat(callback.getLoadSdkErrorCode()).isEqualTo( |
| SdkSandboxManager.LOAD_SDK_ALREADY_LOADED); |
| assertThat(callback.getLoadSdkErrorMsg()).contains("has been loaded already"); |
| } |
| } |
| |
| @Test |
| public void testLoadSdk_errorOnFirstLoad_canBeLoadedAgain() throws Exception { |
| disableNetworkPermissionChecks(); |
| |
| // Load code, but make it fail |
| { |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| // Assume SupplementalProcess load fails |
| mSdkSandboxService.sendLoadCodeError(); |
| assertThat(callback.isLoadSdkSuccessful()).isFalse(); |
| } |
| |
| // Caller should be able to retry loading the code |
| { |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| // Assume SupplementalProcess loads successfully |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback.isLoadSdkSuccessful()).isTrue(); |
| } |
| } |
| |
| @Test |
| public void testRequestSurfacePackageSdkNotLoaded() { |
| // Trying to request package with not exist SDK packageName |
| String sdkName = "invalid"; |
| IllegalArgumentException thrown = |
| assertThrows( |
| IllegalArgumentException.class, |
| () -> |
| mService.requestSurfacePackage( |
| TEST_PACKAGE, |
| sdkName, |
| new Binder(), |
| 0, |
| 500, |
| 500, |
| new Bundle(), |
| new FakeRequestSurfacePackageCallbackBinder())); |
| assertThat(thrown).hasMessageThat().contains("Sdk " + sdkName + " is not loaded"); |
| } |
| |
| @Test |
| public void testRequestSurfacePackage() throws Exception { |
| disableNetworkPermissionChecks(); |
| |
| // 1. We first need to collect a proper sdkToken by calling loadCode |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback.isLoadSdkSuccessful()).isTrue(); |
| |
| // 2. Call request package with the retrieved sdkToken |
| FakeRequestSurfacePackageCallbackBinder surfacePackageCallback = |
| new FakeRequestSurfacePackageCallbackBinder(); |
| mService.requestSurfacePackage( |
| TEST_PACKAGE, |
| SDK_NAME, |
| new Binder(), |
| 0, |
| 500, |
| 500, |
| new Bundle(), |
| surfacePackageCallback); |
| mSdkSandboxService.sendSurfacePackageReady(surfacePackageCallback); |
| assertThat(surfacePackageCallback.isRequestSurfacePackageSuccessful()).isTrue(); |
| } |
| |
| @Test |
| public void testRequestSurfacePackageFailedAfterAppDied() throws Exception { |
| disableKillUid(); |
| disableNetworkPermissionChecks(); |
| |
| FakeLoadSdkCallbackBinder callback = Mockito.spy(new FakeLoadSdkCallbackBinder()); |
| Mockito.doReturn(Mockito.mock(Binder.class)).when(callback).asBinder(); |
| |
| ArgumentCaptor<IBinder.DeathRecipient> deathRecipient = ArgumentCaptor |
| .forClass(IBinder.DeathRecipient.class); |
| |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback.isLoadSdkSuccessful()).isTrue(); |
| |
| Mockito.verify(callback.asBinder()) |
| .linkToDeath(deathRecipient.capture(), ArgumentMatchers.eq(0)); |
| |
| // App Died |
| deathRecipient.getValue().binderDied(); |
| |
| // After App Died |
| IllegalArgumentException thrown = |
| assertThrows( |
| IllegalArgumentException.class, |
| () -> |
| mService.requestSurfacePackage( |
| TEST_PACKAGE, |
| SDK_NAME, |
| new Binder(), |
| 0, |
| 500, |
| 500, |
| new Bundle(), |
| new FakeRequestSurfacePackageCallbackBinder())); |
| assertThat(thrown).hasMessageThat() |
| .contains("Sdk " + SDK_NAME + " is not loaded"); |
| } |
| |
| @Test |
| public void testSurfacePackageError() throws Exception { |
| disableNetworkPermissionChecks(); |
| |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| // Assume SurfacePackage encounters an error. |
| FakeRequestSurfacePackageCallbackBinder surfacePackageCallback = |
| new FakeRequestSurfacePackageCallbackBinder(); |
| mSdkSandboxService.sendSurfacePackageError( |
| SdkSandboxManager.REQUEST_SURFACE_PACKAGE_INTERNAL_ERROR, |
| "bad surface", |
| surfacePackageCallback); |
| assertThat(surfacePackageCallback.getSurfacePackageErrorMsg()).contains("bad surface"); |
| assertThat(surfacePackageCallback.getSurfacePackageErrorCode()) |
| .isEqualTo(SdkSandboxManager.REQUEST_SURFACE_PACKAGE_INTERNAL_ERROR); |
| } |
| |
| @Test |
| public void testSendData_SdkNotLoaded() throws Exception { |
| IllegalArgumentException thrown = |
| assertThrows( |
| IllegalArgumentException.class, |
| () -> |
| mService.sendData( |
| TEST_PACKAGE, |
| SDK_NAME, |
| new Bundle(), |
| new ISendDataCallback.Stub() { |
| @Override |
| public void onSendDataSuccess(Bundle params) |
| throws RemoteException {} |
| |
| @Override |
| public void onSendDataError(int i, String s) |
| throws RemoteException {} |
| })); |
| assertThat(thrown).hasMessageThat().contains("Sdk " + SDK_NAME + " is not loaded"); |
| } |
| |
| @Test |
| public void testAddSdkSandboxLifecycleCallback_BeforeStartingSandbox() throws Exception { |
| disableNetworkPermissionChecks(); |
| |
| // Register for sandbox death event |
| FakeSdkSandboxLifecycleCallbackBinder lifecycleCallback = |
| new FakeSdkSandboxLifecycleCallbackBinder(); |
| mService.addSdkSandboxLifecycleCallback(TEST_PACKAGE, lifecycleCallback); |
| |
| // Load SDK and start the sandbox |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback.isLoadSdkSuccessful()).isTrue(); |
| |
| // Kill the sandbox |
| ArgumentCaptor<IBinder.DeathRecipient> deathRecipientCaptor = |
| ArgumentCaptor.forClass(IBinder.DeathRecipient.class); |
| Mockito.verify(mSdkSandboxService.asBinder(), Mockito.atLeastOnce()) |
| .linkToDeath(deathRecipientCaptor.capture(), ArgumentMatchers.eq(0)); |
| IBinder.DeathRecipient deathRecipient = deathRecipientCaptor.getValue(); |
| deathRecipient.binderDied(); |
| |
| // Check that death is recorded correctly |
| assertThat(lifecycleCallback.isSdkSandboxDeathDetected()).isTrue(); |
| } |
| |
| @Test |
| public void testAddSdkSandboxLifecycleCallback_AfterStartingSandbox() throws Exception { |
| disableNetworkPermissionChecks(); |
| |
| // Load SDK and start the sandbox |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback.isLoadSdkSuccessful()).isTrue(); |
| |
| // Register for sandbox death event |
| FakeSdkSandboxLifecycleCallbackBinder lifecycleCallback = |
| new FakeSdkSandboxLifecycleCallbackBinder(); |
| mService.addSdkSandboxLifecycleCallback(TEST_PACKAGE, lifecycleCallback); |
| |
| // Kill the sandbox |
| ArgumentCaptor<IBinder.DeathRecipient> deathRecipientCaptor = |
| ArgumentCaptor.forClass(IBinder.DeathRecipient.class); |
| Mockito.verify(mSdkSandboxService.asBinder(), Mockito.atLeastOnce()) |
| .linkToDeath(deathRecipientCaptor.capture(), ArgumentMatchers.eq(0)); |
| IBinder.DeathRecipient deathRecipient = deathRecipientCaptor.getValue(); |
| deathRecipient.binderDied(); |
| |
| // Check that death is recorded correctly |
| assertThat(lifecycleCallback.isSdkSandboxDeathDetected()).isTrue(); |
| } |
| |
| @Test |
| public void testMultipleAddSdkSandboxLifecycleCallbacks() throws Exception { |
| disableNetworkPermissionChecks(); |
| |
| // Register for sandbox death event |
| FakeSdkSandboxLifecycleCallbackBinder lifecycleCallback1 = |
| new FakeSdkSandboxLifecycleCallbackBinder(); |
| mService.addSdkSandboxLifecycleCallback(TEST_PACKAGE, lifecycleCallback1); |
| |
| // Load SDK and start the sandbox |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback.isLoadSdkSuccessful()).isTrue(); |
| |
| // Register for sandbox death event again |
| FakeSdkSandboxLifecycleCallbackBinder lifecycleCallback2 = |
| new FakeSdkSandboxLifecycleCallbackBinder(); |
| mService.addSdkSandboxLifecycleCallback(TEST_PACKAGE, lifecycleCallback2); |
| |
| // Kill the sandbox |
| ArgumentCaptor<IBinder.DeathRecipient> deathRecipientCaptor = |
| ArgumentCaptor.forClass(IBinder.DeathRecipient.class); |
| Mockito.verify(mSdkSandboxService.asBinder(), Mockito.atLeastOnce()) |
| .linkToDeath(deathRecipientCaptor.capture(), ArgumentMatchers.eq(0)); |
| IBinder.DeathRecipient deathRecipient = deathRecipientCaptor.getValue(); |
| deathRecipient.binderDied(); |
| |
| // Check that death is recorded correctly |
| assertThat(lifecycleCallback1.isSdkSandboxDeathDetected()).isTrue(); |
| assertThat(lifecycleCallback2.isSdkSandboxDeathDetected()).isTrue(); |
| } |
| |
| @Test |
| public void testRemoveSdkSandboxLifecycleCallback() throws Exception { |
| disableNetworkPermissionChecks(); |
| |
| // Load SDK and start the sandbox |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback.isLoadSdkSuccessful()).isTrue(); |
| |
| // Register for sandbox death event |
| FakeSdkSandboxLifecycleCallbackBinder lifecycleCallback1 = |
| new FakeSdkSandboxLifecycleCallbackBinder(); |
| mService.addSdkSandboxLifecycleCallback(TEST_PACKAGE, lifecycleCallback1); |
| |
| // Register for sandbox death event again |
| FakeSdkSandboxLifecycleCallbackBinder lifecycleCallback2 = |
| new FakeSdkSandboxLifecycleCallbackBinder(); |
| mService.addSdkSandboxLifecycleCallback(TEST_PACKAGE, lifecycleCallback2); |
| |
| // Unregister one of the lifecycle callbacks |
| mService.removeSdkSandboxLifecycleCallback(TEST_PACKAGE, lifecycleCallback1); |
| |
| // Kill the sandbox |
| ArgumentCaptor<IBinder.DeathRecipient> deathRecipientCaptor = |
| ArgumentCaptor.forClass(IBinder.DeathRecipient.class); |
| Mockito.verify(mSdkSandboxService.asBinder(), Mockito.atLeastOnce()) |
| .linkToDeath(deathRecipientCaptor.capture(), ArgumentMatchers.eq(0)); |
| IBinder.DeathRecipient deathRecipient = deathRecipientCaptor.getValue(); |
| deathRecipient.binderDied(); |
| |
| // Check that death is recorded correctly |
| assertThat(lifecycleCallback1.isSdkSandboxDeathDetected()).isFalse(); |
| assertThat(lifecycleCallback2.isSdkSandboxDeathDetected()).isTrue(); |
| } |
| |
| @Test(expected = SecurityException.class) |
| public void testDumpWithoutPermission() { |
| mService.dump(new FileDescriptor(), new PrintWriter(new StringWriter()), new String[0]); |
| } |
| |
| @Test |
| public void testSdkSandboxServiceUnbindingWhenAppDied() throws Exception { |
| disableKillUid(); |
| disableNetworkPermissionChecks(); |
| |
| ILoadSdkCallback.Stub callback = Mockito.spy(ILoadSdkCallback.Stub.class); |
| int callingUid = Binder.getCallingUid(); |
| final CallingInfo callingInfo = new CallingInfo(callingUid, TEST_PACKAGE); |
| assertThat(mProvider.getBoundServiceForApp(callingInfo)).isNull(); |
| |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| |
| ArgumentCaptor<IBinder.DeathRecipient> deathRecipient = ArgumentCaptor |
| .forClass(IBinder.DeathRecipient.class); |
| Mockito.verify(callback.asBinder(), Mockito.times(1)) |
| .linkToDeath(deathRecipient.capture(), Mockito.eq(0)); |
| |
| assertThat(mProvider.getBoundServiceForApp(callingInfo)).isNotNull(); |
| deathRecipient.getValue().binderDied(); |
| assertThat(mProvider.getBoundServiceForApp(callingInfo)).isNull(); |
| } |
| |
| /* Tests resources defined in CodeProviderWithResources may be read. */ |
| @Test |
| public void testCodeContextResourcesAndAssets() throws Exception { |
| Context context = InstrumentationRegistry.getInstrumentation().getContext(); |
| PackageManager pm = context.getPackageManager(); |
| ApplicationInfo info = pm.getApplicationInfo(SDK_PROVIDER_RESOURCES_PACKAGE, |
| PackageManager.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES); |
| assertThat(info).isNotNull(); |
| SandboxedSdkContext sandboxedSdkContext = |
| new SandboxedSdkContext(context, CLIENT_PACKAGE_NAME, info, SDK_NAME, null, null); |
| Resources resources = sandboxedSdkContext.getResources(); |
| |
| int integerId = resources.getIdentifier("test_integer", "integer", |
| SDK_PROVIDER_RESOURCES_PACKAGE); |
| assertThat(integerId).isNotEqualTo(0); |
| assertThat(resources.getInteger(integerId)).isEqualTo(1234); |
| |
| int stringId = resources.getIdentifier("test_string", "string", |
| SDK_PROVIDER_RESOURCES_PACKAGE); |
| assertThat(stringId).isNotEqualTo(0); |
| assertThat(resources.getString(stringId)).isEqualTo("Test String"); |
| |
| AssetManager assetManager = resources.getAssets(); |
| BufferedReader reader = new BufferedReader( |
| new InputStreamReader(assetManager.open("test-asset.txt"))); |
| assertThat(reader.readLine()).isEqualTo("This is a test asset"); |
| } |
| |
| /** Tests that only allowed intents may be sent from the sdk sandbox. */ |
| @Test |
| public void testEnforceAllowedToSendBroadcast() { |
| SdkSandboxManagerLocal mSdkSandboxManagerLocal = mService.getLocalManager(); |
| |
| Intent disallowedIntent = new Intent(Intent.ACTION_SCREEN_ON); |
| assertThrows(SecurityException.class, |
| () -> mSdkSandboxManagerLocal.enforceAllowedToSendBroadcast(disallowedIntent)); |
| } |
| |
| /** Tests that only allowed activities may be started from the sdk sandbox. */ |
| @Test |
| public void testEnforceAllowedToStartActivity() { |
| SdkSandboxManagerLocal mSdkSandboxManagerLocal = mService.getLocalManager(); |
| Intent allowedIntent = new Intent(Intent.ACTION_VIEW); |
| mSdkSandboxManagerLocal.enforceAllowedToStartActivity(allowedIntent); |
| |
| Intent disallowedIntent = new Intent(Intent.ACTION_SCREEN_OFF); |
| assertThrows(SecurityException.class, |
| () -> mSdkSandboxManagerLocal.enforceAllowedToStartActivity(disallowedIntent)); |
| } |
| |
| @Test |
| public void testGetSdkSandboxProcessNameForInstrumentation() throws Exception { |
| final SdkSandboxManagerLocal localManager = mService.getLocalManager(); |
| final PackageManager pm = |
| InstrumentationRegistry.getInstrumentation().getContext().getPackageManager(); |
| final ApplicationInfo info = pm.getApplicationInfo(TEST_PACKAGE, 0); |
| final String processName = localManager.getSdkSandboxProcessNameForInstrumentation(info); |
| assertThat(processName).isEqualTo(TEST_PACKAGE + "_sdk_sandbox_instr"); |
| } |
| |
| @Test |
| public void testNotifyInstrumentationStarted_killsSandboxProcess() throws Exception { |
| disableKillUid(); |
| disableNetworkPermissionChecks(); |
| |
| // First load SDK. |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback.isLoadSdkSuccessful()).isTrue(); |
| |
| final CallingInfo callingInfo = new CallingInfo(Process.myUid(), TEST_PACKAGE); |
| |
| // Check that sdk sandbox for TEST_PACKAGE is bound |
| assertThat(mProvider.getBoundServiceForApp(callingInfo)).isNotNull(); |
| |
| final SdkSandboxManagerLocal localManager = mService.getLocalManager(); |
| localManager.notifyInstrumentationStarted(TEST_PACKAGE, Process.myUid()); |
| |
| // Verify that sdk sandbox was killed |
| Mockito.verify(mAmSpy, Mockito.only()) |
| .killUid(Mockito.eq(Process.toSdkSandboxUid(Process.myUid())), Mockito.anyString()); |
| assertThat(mProvider.getBoundServiceForApp(callingInfo)).isNull(); |
| } |
| |
| @Test |
| public void testNotifyInstrumentationStarted_doesNotAllowLoadSdk() throws Exception { |
| disableKillUid(); |
| disableNetworkPermissionChecks(); |
| |
| // First load SDK. |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback.isLoadSdkSuccessful()).isTrue(); |
| |
| final CallingInfo callingInfo = new CallingInfo(Process.myUid(), TEST_PACKAGE); |
| |
| // Check that sdk sandbox for TEST_PACKAGE is bound |
| assertThat(mProvider.getBoundServiceForApp(callingInfo)).isNotNull(); |
| |
| final SdkSandboxManagerLocal localManager = mService.getLocalManager(); |
| localManager.notifyInstrumentationStarted(TEST_PACKAGE, Process.myUid()); |
| assertThat(mProvider.getBoundServiceForApp(callingInfo)).isNull(); |
| |
| // Try load again, it should throw SecurityException |
| FakeLoadSdkCallbackBinder callback2 = new FakeLoadSdkCallbackBinder(); |
| SecurityException e = assertThrows( |
| SecurityException.class, |
| () -> mService.loadSdk( |
| TEST_PACKAGE, SDK_NAME, new Bundle(), callback2)); |
| assertThat(e).hasMessageThat() |
| .contains("Currently running instrumentation of this sdk sandbox process"); |
| } |
| |
| @Test |
| public void testNotifyInstrumentationFinished_canLoadSdk() throws Exception { |
| disableKillUid(); |
| disableNetworkPermissionChecks(); |
| |
| final SdkSandboxManagerLocal localManager = mService.getLocalManager(); |
| localManager.notifyInstrumentationStarted(TEST_PACKAGE, Process.myUid()); |
| |
| final CallingInfo callingInfo = new CallingInfo(Process.myUid(), TEST_PACKAGE); |
| assertThat(mProvider.getBoundServiceForApp(callingInfo)).isNull(); |
| |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| // Try loading, it should throw SecurityException |
| SecurityException e = assertThrows( |
| SecurityException.class, |
| () -> mService.loadSdk(TEST_PACKAGE, SDK_NAME, |
| new Bundle(), callback)); |
| assertThat(e).hasMessageThat() |
| .contains("Currently running instrumentation of this sdk sandbox process"); |
| |
| localManager.notifyInstrumentationFinished(TEST_PACKAGE, Process.myUid()); |
| |
| FakeLoadSdkCallbackBinder callback2 = new FakeLoadSdkCallbackBinder(); |
| // Now loading should work |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback2); |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback2.isLoadSdkSuccessful()).isTrue(); |
| assertThat(mProvider.getBoundServiceForApp(callingInfo)).isNotNull(); |
| } |
| |
| @Test |
| public void testGetLoadedSdkLibrariesInfo_afterLoadSdkSuccess() throws Exception { |
| disableNetworkPermissionChecks(); |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| // Now loading should work |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback.isLoadSdkSuccessful()).isTrue(); |
| assertThat(mService.getLoadedSdkLibrariesInfo(TEST_PACKAGE)).hasSize(1); |
| assertThat(mService.getLoadedSdkLibrariesInfo(TEST_PACKAGE).get(0).getName()) |
| .isEqualTo(SDK_NAME); |
| } |
| |
| @Test |
| public void testGetLoadedSdkLibrariesInfo_errorLoadingSdk() throws Exception { |
| disableNetworkPermissionChecks(); |
| |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| mSdkSandboxService.sendLoadCodeError(); |
| |
| // Verify sdkInfo is missing when loading failed |
| assertThat(callback.isLoadSdkSuccessful()).isFalse(); |
| assertThat(callback.getLoadSdkErrorCode()) |
| .isEqualTo(SdkSandboxManager.LOAD_SDK_INTERNAL_ERROR); |
| assertThat(mService.getLoadedSdkLibrariesInfo(TEST_PACKAGE)).isEmpty(); |
| } |
| |
| @Test |
| public void testEnforceAllowedToStartOrBindService() { |
| SdkSandboxManagerLocal mSdkSandboxManagerLocal = mService.getLocalManager(); |
| |
| Intent disallowedIntent = new Intent(); |
| disallowedIntent.setComponent(new ComponentName("nonexistent.package", "test")); |
| assertThrows(SecurityException.class, |
| () -> mSdkSandboxManagerLocal.enforceAllowedToStartOrBindService(disallowedIntent)); |
| } |
| |
| @Test |
| public void testAdServicesPackageIsResolved() { |
| assertThat(mService.getAdServicesPackageName()).contains("adservices"); |
| } |
| |
| @Test |
| public void testUnloadSdkThatIsNotLoaded() { |
| assertThrows( |
| IllegalArgumentException.class, () -> mService.unloadSdk(TEST_PACKAGE, SDK_NAME)); |
| } |
| |
| @Test |
| public void testUnloadSdkThatIsLoaded() throws Exception { |
| disableNetworkPermissionChecks(); |
| disableKillUid(); |
| |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback.isLoadSdkSuccessful()).isTrue(); |
| |
| FakeLoadSdkCallbackBinder callback2 = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_PROVIDER_RESOURCES_SDK_NAME, new Bundle(), callback2); |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback2.isLoadSdkSuccessful()).isTrue(); |
| |
| final CallingInfo callingInfo = new CallingInfo(Process.myUid(), TEST_PACKAGE); |
| mService.unloadSdk(TEST_PACKAGE, SDK_NAME); |
| |
| // One SDK should still be loaded, therefore the sandbox should still be alive. |
| assertThat(mProvider.getBoundServiceForApp(callingInfo)).isNotNull(); |
| |
| mService.unloadSdk(TEST_PACKAGE, SDK_PROVIDER_RESOURCES_SDK_NAME); |
| |
| // No more SDKs should be loaded at this point. Verify that the sandbox has been killed. |
| Mockito.verify(mAmSpy, Mockito.only()) |
| .killUid(Mockito.eq(Process.toSdkSandboxUid(Process.myUid())), Mockito.anyString()); |
| assertThat(mProvider.getBoundServiceForApp(callingInfo)).isNull(); |
| } |
| |
| @Test |
| public void test_syncDataFromClient_verifiesCallingPackageName() { |
| SecurityException thrown = |
| assertThrows( |
| SecurityException.class, |
| () -> |
| mService.loadSdk( |
| "does.not.exist", |
| SDK_NAME, |
| new Bundle(), |
| new FakeLoadSdkCallbackBinder())); |
| assertThat(thrown).hasMessageThat().contains("does.not.exist not found"); |
| } |
| |
| @Test |
| public void test_syncDataFromClient_sandboxServiceIsNotBound() { |
| // Sync data from client |
| mService.syncDataFromClient(TEST_PACKAGE, new Bundle()); |
| |
| // Verify when sandbox is not bound, manager service does not try to sync |
| assertThat(mSdkSandboxService.getLastUpdate()).isNull(); |
| } |
| |
| @Test |
| public void test_syncDataFromClient_sandboxServiceIsAlreadyBound() { |
| // Ensure a sandbox service is already bound for the client |
| final CallingInfo callingInfo = new CallingInfo(Process.myUid(), TEST_PACKAGE); |
| mProvider.bindService(callingInfo, Mockito.mock(ServiceConnection.class)); |
| |
| // Sync data from client |
| final Bundle data = new Bundle(); |
| mService.syncDataFromClient(TEST_PACKAGE, data); |
| |
| // Verify that manager service calls sandbox to sync data |
| assertThat(mSdkSandboxService.getLastUpdate()).isSameInstanceAs(data); |
| } |
| |
| @Test |
| public void testStopSdkSandbox() throws Exception { |
| disableKillUid(); |
| disableNetworkPermissionChecks(); |
| |
| FakeLoadSdkCallbackBinder callback = new FakeLoadSdkCallbackBinder(); |
| mService.loadSdk(TEST_PACKAGE, SDK_NAME, new Bundle(), callback); |
| // Assume sandbox loads successfully |
| mSdkSandboxService.sendLoadCodeSuccessful(); |
| assertThat(callback.isLoadSdkSuccessful()).isTrue(); |
| |
| Mockito.doNothing() |
| .when(mSpyContext) |
| .enforceCallingPermission( |
| Mockito.eq("com.android.app.sdksandbox.permission.STOP_SDK_SANDBOX"), |
| Mockito.anyString()); |
| mService.stopSdkSandbox(TEST_PACKAGE); |
| int callingUid = Binder.getCallingUid(); |
| final CallingInfo callingInfo = new CallingInfo(callingUid, TEST_PACKAGE); |
| assertThat(mProvider.getBoundServiceForApp(callingInfo)).isEqualTo(null); |
| } |
| |
| @Test(expected = SecurityException.class) |
| public void testStopSdkSandbox_WithoutPermission() { |
| mService.stopSdkSandbox(TEST_PACKAGE); |
| } |
| |
| /** |
| * Fake service provider that returns local instance of {@link SdkSandboxServiceProvider} |
| */ |
| private static class FakeSdkSandboxProvider implements SdkSandboxServiceProvider { |
| private final ISdkSandboxService mSdkSandboxService; |
| private final ArrayMap<CallingInfo, ISdkSandboxService> mService = |
| new ArrayMap<>(); |
| |
| FakeSdkSandboxProvider(ISdkSandboxService service) { |
| mSdkSandboxService = service; |
| } |
| |
| @Override |
| public void bindService(CallingInfo callingInfo, ServiceConnection serviceConnection) { |
| if (mService.containsKey(callingInfo)) { |
| return; |
| } |
| mService.put(callingInfo, mSdkSandboxService); |
| serviceConnection.onServiceConnected(null, mSdkSandboxService.asBinder()); |
| } |
| |
| @Override |
| public void unbindService(CallingInfo callingInfo) { |
| mService.remove(callingInfo); |
| } |
| |
| @Nullable |
| @Override |
| public ISdkSandboxService getBoundServiceForApp(CallingInfo callingInfo) { |
| return mService.get(callingInfo); |
| } |
| |
| @Override |
| public void setBoundServiceForApp(CallingInfo callingInfo, |
| @Nullable ISdkSandboxService service) { |
| mService.put(callingInfo, service); |
| } |
| } |
| |
| public static class FakeSdkSandboxService extends ISdkSandboxService.Stub { |
| private ILoadSdkInSandboxCallback mLoadSdkInSandboxCallback; |
| private final ISdkSandboxManagerToSdkSandboxCallback mManagerToSdkCallback; |
| |
| private boolean mSurfacePackageRequested = false; |
| private Bundle mLastSyncUpdate = null; |
| |
| FakeSdkSandboxService() { |
| mManagerToSdkCallback = new FakeManagerToSdkCallback(); |
| } |
| |
| @Override |
| public void loadSdk( |
| String callingPackageName, |
| IBinder codeToken, |
| ApplicationInfo info, |
| String sdkName, |
| String sdkProviderClass, |
| String ceDataDir, |
| String deDataDir, |
| Bundle params, |
| ILoadSdkInSandboxCallback callback) { |
| mLoadSdkInSandboxCallback = callback; |
| } |
| |
| @Override |
| public void unloadSdk(IBinder sdkToken) {} |
| |
| @Override |
| public void syncDataFromClient(Bundle data) { |
| mLastSyncUpdate = data; |
| } |
| |
| @Nullable |
| public Bundle getLastUpdate() { |
| return mLastSyncUpdate; |
| } |
| |
| void sendLoadCodeSuccessful() throws RemoteException { |
| mLoadSdkInSandboxCallback.onLoadSdkSuccess(new Bundle(), mManagerToSdkCallback); |
| } |
| |
| void sendLoadCodeError() throws RemoteException { |
| mLoadSdkInSandboxCallback.onLoadSdkError( |
| SdkSandboxManager.LOAD_SDK_INTERNAL_ERROR, "Internal error"); |
| } |
| |
| void sendSurfacePackageReady(FakeRequestSurfacePackageCallbackBinder callback) |
| throws RemoteException { |
| if (mSurfacePackageRequested) { |
| callback.onSurfacePackageReady( |
| /*hostToken=*/ null, /*displayId=*/ 0, /*params=*/ null); |
| } |
| } |
| |
| void sendSurfacePackageError( |
| int errorCode, String errorMsg, FakeRequestSurfacePackageCallbackBinder callback) |
| throws RemoteException { |
| callback.onSurfacePackageError(errorCode, errorMsg); |
| } |
| |
| private class FakeManagerToSdkCallback extends ISdkSandboxManagerToSdkSandboxCallback.Stub { |
| @Override |
| public void onSurfacePackageRequested( |
| IBinder hostToken, |
| int displayId, |
| int width, |
| int height, |
| Bundle extraParams, |
| IRequestSurfacePackageFromSdkCallback iRequestSurfacePackageFromSdkCallback) { |
| mSurfacePackageRequested = true; |
| } |
| |
| @Override |
| public void onDataReceived(Bundle data, IDataReceivedCallback callback) {} |
| } |
| } |
| } |