diff options
| author | android-build-prod (mdb) <android-build-team-robot@google.com> | 2020-09-17 17:54:36 +0000 |
|---|---|---|
| committer | android-build-prod (mdb) <android-build-team-robot@google.com> | 2020-09-17 17:54:36 +0000 |
| commit | 15fbfb09b48107723939599d1a8d0043d3f1c1cc (patch) | |
| tree | 62c08a00334878f5774e4622ced16ce5d7b31153 | |
| parent | 05a06c3d05d5bd64c0403d0e88952f0f87bd0e52 (diff) | |
| parent | 5bc1596595c5884dd022f29ac273e6ed542e783a (diff) | |
| download | platform_external_robolectric-shadows-simpleperf-release.tar.gz platform_external_robolectric-shadows-simpleperf-release.tar.bz2 platform_external_robolectric-shadows-simpleperf-release.zip | |
Snap for 6844436 from 5bc1596595c5884dd022f29ac273e6ed542e783a to simpleperf-releasesimpleperf-release
Change-Id: I46adc50ea5f07281c9a1721d32bd665d6cff1d26
34 files changed, 1890 insertions, 143 deletions
diff --git a/Android.bp b/Android.bp index 88fa2a526..f1dda073e 100644 --- a/Android.bp +++ b/Android.bp @@ -102,6 +102,10 @@ java_library_host { // Copy the build.prop ":robolectric_build_props", ], + visibility: [ + ":__subpackages__", + "//prebuilts/misc/common/robolectric", + ], } //############################################# diff --git a/gen_test_config.mk b/gen_test_config.mk index b478e833b..48c346ac0 100644 --- a/gen_test_config.mk +++ b/gen_test_config.mk @@ -12,7 +12,7 @@ LOCAL_JAVA_RESOURCE_FILES += $(test_config_dir):com/android/tools/test_config.pr # Define variables to be written into the generated test_config.properties file. android_merged_manifest := $(strip $(call intermediates-dir-for,APPS,$(LOCAL_INSTRUMENTATION_FOR),,COMMON)/manifest/AndroidManifest.xml) -android_resource_apk := $(strip $(call apk-location-for,$(LOCAL_INSTRUMENTATION_FOR))) +android_resource_apk := $(strip $(call intermediates-dir-for,APPS,$(LOCAL_INSTRUMENTATION_FOR))/package.apk) # Snapshot the written variables so they cannot be polluted before the module is built. $(test_config_file): private_android_merged_manifest := $(android_merged_manifest) diff --git a/processor/sdks.txt b/processor/sdks.txt index c6f88b4b1..e61922009 100644 --- a/processor/sdks.txt +++ b/processor/sdks.txt @@ -10,3 +10,4 @@ prebuilts/misc/common/robolectric/android-all/android-all-7.1.0_r7-robolectric-r prebuilts/misc/common/robolectric/android-all/android-all-8.0.0_r4-robolectric-r1.jar prebuilts/misc/common/robolectric/android-all/android-all-8.1.0-robolectric-4611349.jar prebuilts/misc/common/robolectric/android-all/android-all-9-robolectric-4913185-2.jar +prebuilts/misc/common/robolectric/android-all/android-all-9plus-robolectric-5616371.jar diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAppOpsManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAppOpsManagerTest.java index 631d28020..81a25062b 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAppOpsManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAppOpsManagerTest.java @@ -5,6 +5,7 @@ import static android.app.AppOpsManager.MODE_DEFAULT; import static android.app.AppOpsManager.MODE_ERRORED; import static android.app.AppOpsManager.OPSTR_FINE_LOCATION; import static android.app.AppOpsManager.OPSTR_GPS; +import static android.app.AppOpsManager.OPSTR_SEND_SMS; import static android.app.AppOpsManager.OP_FINE_LOCATION; import static android.app.AppOpsManager.OP_GPS; import static android.app.AppOpsManager.OP_SEND_SMS; @@ -101,24 +102,28 @@ public class ShadowAppOpsManagerTest { } @Test - @Config(minSdk = VERSION_CODES.O_MR1) - public void noModeSet_atLeastO_noteProxyOpNoThrow_shouldReturnModeAllowed() { - assertThat(appOps.noteProxyOpNoThrow(OP_GPS, PACKAGE_NAME1)).isEqualTo(MODE_ALLOWED); + @Config(minSdk = VERSION_CODES.R) + public void noModeSet_atLeastR_noteProxyOpNoThrow_shouldReturnModeAllowed() { + assertThat(appOps.noteProxyOpNoThrow(OPSTR_GPS, PACKAGE_NAME1, UID_1, null, null)) + .isEqualTo(MODE_ALLOWED); } @Test - @Config(minSdk = VERSION_CODES.O_MR1) - public void setMode_withModeDefault_atLeastO_noteProxyOpNoThrow_shouldReturnModeDefault() { + @Config(minSdk = VERSION_CODES.R) + public void setMode_withModeDefault_atLeastR_noteProxyOpNoThrow_shouldReturnModeDefault() { appOps.setMode(OP_GPS, Binder.getCallingUid(), PACKAGE_NAME1, MODE_DEFAULT); - assertThat(appOps.noteProxyOpNoThrow(OP_GPS, PACKAGE_NAME1)).isEqualTo(MODE_DEFAULT); + assertThat(appOps.noteProxyOpNoThrow(OPSTR_GPS, PACKAGE_NAME1, UID_1, null, null)) + .isEqualTo(MODE_DEFAULT); } @Test - @Config(minSdk = VERSION_CODES.P) - public void setMode_noteProxyOpNoThrow_atLeastO() { - assertThat(appOps.noteProxyOpNoThrow(OP_GPS, PACKAGE_NAME1)).isEqualTo(MODE_ALLOWED); + @Config(minSdk = VERSION_CODES.R) + public void setMode_noteProxyOpNoThrow_atLeastR() { + assertThat(appOps.noteProxyOpNoThrow(OPSTR_GPS, PACKAGE_NAME1, UID_1, null, null)) + .isEqualTo(MODE_ALLOWED); appOps.setMode(OP_GPS, Binder.getCallingUid(), PACKAGE_NAME1, MODE_ERRORED); - assertThat(appOps.noteProxyOpNoThrow(OP_GPS, PACKAGE_NAME1)).isEqualTo(MODE_ERRORED); + assertThat(appOps.noteProxyOpNoThrow(OPSTR_GPS, PACKAGE_NAME1, UID_1, null, null)) + .isEqualTo(MODE_ERRORED); } @Test @@ -135,6 +140,7 @@ public class ShadowAppOpsManagerTest { } @Test + @Config(maxSdk = VERSION_CODES.Q) public void noteOp() { assertThat(appOps.noteOp(OP_GPS, UID_1, PACKAGE_NAME1)).isEqualTo(MODE_ALLOWED); // Use same op more than once @@ -144,12 +150,31 @@ public class ShadowAppOpsManagerTest { } @Test + @Config(minSdk = VERSION_CODES.R) + public void noteOp_atLeastR() { + assertThat(appOps.noteOp(OPSTR_GPS, UID_1, PACKAGE_NAME1, null, null)).isEqualTo(MODE_ALLOWED); + // Use same op more than once + assertThat(appOps.noteOp(OPSTR_GPS, UID_1, PACKAGE_NAME1, null, null)).isEqualTo(MODE_ALLOWED); + + assertThat(appOps.noteOp(OPSTR_SEND_SMS, UID_1, PACKAGE_NAME1, null, null)).isEqualTo(MODE_ALLOWED); + } + + @Test + @Config(maxSdk = VERSION_CODES.Q) public void getOpsForPackage_noOps() { List<PackageOps> results = appOps.getOpsForPackage(UID_1, PACKAGE_NAME1, NO_OP_FILTER); assertOps(results); } @Test + @Config(minSdk = VERSION_CODES.R) + public void getOpsForPackage_noOps_atLeastR() { + List<PackageOps> results = appOps.getOpsForPackage(UID_1, PACKAGE_NAME1); + assertOps(results); + } + + @Test + @Config(maxSdk = VERSION_CODES.Q) public void getOpsForPackage_hasOps() { appOps.noteOp(OP_GPS, UID_1, PACKAGE_NAME1); appOps.noteOp(OP_SEND_SMS, UID_1, PACKAGE_NAME1); @@ -164,6 +189,22 @@ public class ShadowAppOpsManagerTest { } @Test + @Config(minSdk = VERSION_CODES.R) + public void getOpsForPackage_hasOps_atLeastR() { + appOps.noteOp(OPSTR_GPS, UID_1, PACKAGE_NAME1, null, null); + appOps.noteOp(OPSTR_SEND_SMS, UID_1, PACKAGE_NAME1, null, null); + + // PACKAGE_NAME2 has ops. + List<PackageOps> results = appOps.getOpsForPackage(UID_1, PACKAGE_NAME1); + assertOps(results, OP_GPS, OP_SEND_SMS); + + // PACKAGE_NAME2 has no ops. + results = appOps.getOpsForPackage(UID_2, PACKAGE_NAME2); + assertOps(results); + } + + @Test + @Config(maxSdk = VERSION_CODES.Q) public void getOpsForPackage_withOpFilter() { List<PackageOps> results = appOps.getOpsForPackage(UID_1, PACKAGE_NAME1, new int[] {OP_GPS}); assertOps(results); @@ -178,6 +219,21 @@ public class ShadowAppOpsManagerTest { } @Test + @Config(minSdk = VERSION_CODES.R) + public void getOpsForPackage_withOpFilter_atLeastR() { + List<PackageOps> results = appOps.getOpsForPackage(UID_1, PACKAGE_NAME1, OPSTR_GPS); + assertOps(results); + + appOps.noteOp(OPSTR_SEND_SMS, UID_1, PACKAGE_NAME1, null, null); + results = appOps.getOpsForPackage(UID_1, PACKAGE_NAME1, OPSTR_GPS); + assertOps(results); + + appOps.noteOp(OPSTR_GPS, UID_1, PACKAGE_NAME1, null, null); + results = appOps.getOpsForPackage(UID_1, PACKAGE_NAME1, OPSTR_GPS); + assertOps(results, OP_GPS); + } + + @Test @Config(minSdk = VERSION_CODES.LOLLIPOP) public void setRestrictions() { appOps.setRestriction( diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextImplTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextImplTest.java index 77ae3e48e..fd5adab9b 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextImplTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextImplTest.java @@ -16,6 +16,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.content.ServiceConnection; +import android.os.Bundle; import android.os.IBinder; import android.os.Process; import android.view.LayoutInflater; @@ -218,4 +219,17 @@ public class ShadowContextImplTest { public void onServiceDisconnected(ComponentName name) {} }; } + + @Test + @Config(minSdk = LOLLIPOP) + public void startActivityAsUser() { + Intent intent = new Intent(); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Bundle options = new Bundle(); + + context.startActivityAsUser(intent, options, Process.myUserHandle()); + + Intent launchedActivityIntent = shadowOf(context).getNextStartedActivity(); + assertThat(launchedActivityIntent).isEqualTo(intent); + } } diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java index d30abb2f8..3ccd80cb8 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java @@ -2,6 +2,8 @@ package org.robolectric.shadows; import static android.content.pm.PackageManager.PERMISSION_DENIED; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.os.Build.VERSION_CODES.KITKAT; +import static android.os.Build.VERSION_CODES.M; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; @@ -24,6 +26,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; +import android.os.UserHandle; import android.view.LayoutInflater; import android.view.View; import androidx.test.core.app.ApplicationProvider; @@ -39,6 +42,8 @@ import org.robolectric.R; import org.robolectric.Robolectric; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.ReflectionHelpers.ClassParameter; @RunWith(AndroidJUnit4.class) public class ShadowContextWrapperTest { @@ -150,6 +155,92 @@ public class ShadowContextWrapperTest { assertThat(resultReceiver.resultCode).isEqualTo(1); } + @Test + @Config(minSdk = KITKAT) + public void sendOrderedBroadcastAsUser_shouldReturnValues() throws Exception { + String action = "test"; + + IntentFilter lowFilter = new IntentFilter(action); + lowFilter.setPriority(1); + BroadcastReceiver lowReceiver = broadcastReceiver("Low"); + contextWrapper.registerReceiver(lowReceiver, lowFilter); + + IntentFilter highFilter = new IntentFilter(action); + highFilter.setPriority(2); + BroadcastReceiver highReceiver = broadcastReceiver("High"); + contextWrapper.registerReceiver(highReceiver, highFilter); + + final FooReceiver resultReceiver = new FooReceiver(); + contextWrapper.sendOrderedBroadcastAsUser( + new Intent(action), null, null, resultReceiver, null, 1, "initial", null); + assertThat(transcript).containsExactly("High notified of test", "Low notified of test"); + assertThat(resultReceiver.resultCode).isEqualTo(1); + } + + @Test + @Config(minSdk = M) + public void sendOrderedBroadcastAsUser_withAppOp_shouldReturnValues() throws Exception { + String action = "test"; + + IntentFilter lowFilter = new IntentFilter(action); + lowFilter.setPriority(1); + BroadcastReceiver lowReceiver = broadcastReceiver("Low"); + contextWrapper.registerReceiver(lowReceiver, lowFilter); + + IntentFilter highFilter = new IntentFilter(action); + highFilter.setPriority(2); + BroadcastReceiver highReceiver = broadcastReceiver("High"); + contextWrapper.registerReceiver(highReceiver, highFilter); + + final FooReceiver resultReceiver = new FooReceiver(); + + ReflectionHelpers.callInstanceMethod(contextWrapper, "sendOrderedBroadcastAsUser", + ClassParameter.from(Intent.class, new Intent(action)), + ClassParameter.from(UserHandle.class, null), + ClassParameter.from(String.class, null), + ClassParameter.from(int.class, 1), + ClassParameter.from(BroadcastReceiver.class, resultReceiver), + ClassParameter.from(Handler.class, null), + ClassParameter.from(int.class, 1), + ClassParameter.from(String.class, "initial"), + ClassParameter.from(Bundle.class, null) + ); + + assertThat(transcript).containsExactly("High notified of test", "Low notified of test"); + assertThat(resultReceiver.resultCode).isEqualTo(1); + } + + @Test + @Config(minSdk = M) + public void sendOrderedBroadcastAsUser_withAppOpAndOptions_shouldReturnValues() throws Exception { + String action = "test"; + + IntentFilter lowFilter = new IntentFilter(action); + lowFilter.setPriority(1); + BroadcastReceiver lowReceiver = broadcastReceiver("Low"); + contextWrapper.registerReceiver(lowReceiver, lowFilter); + + IntentFilter highFilter = new IntentFilter(action); + highFilter.setPriority(2); + BroadcastReceiver highReceiver = broadcastReceiver("High"); + contextWrapper.registerReceiver(highReceiver, highFilter); + + final FooReceiver resultReceiver = new FooReceiver(); + + ReflectionHelpers.callInstanceMethod(contextWrapper, "sendOrderedBroadcastAsUser", + ClassParameter.from(Intent.class, new Intent(action)), + ClassParameter.from(UserHandle.class, null), + ClassParameter.from(String.class, null), + ClassParameter.from(int.class, 1), + ClassParameter.from(Bundle.class, null), + ClassParameter.from(BroadcastReceiver.class, resultReceiver), + ClassParameter.from(Handler.class, null), + ClassParameter.from(int.class, 1), + ClassParameter.from(String.class, "initial"), + ClassParameter.from(Bundle.class, null) + ); + } + private static final class FooReceiver extends BroadcastReceiver { private int resultCode; private SettableFuture<Void> settableFuture = SettableFuture.create(); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowCrossProfileAppsTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowCrossProfileAppsTest.java new file mode 100644 index 000000000..cae91513a --- /dev/null +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowCrossProfileAppsTest.java @@ -0,0 +1,397 @@ +package org.robolectric.shadows; + +import static android.Manifest.permission.INTERACT_ACROSS_PROFILES; +import static android.app.AppOpsManager.MODE_ALLOWED; +import static android.app.AppOpsManager.MODE_DEFAULT; +import static android.os.Build.VERSION_CODES.P; +import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Application; +import android.app.AppOpsManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.CrossProfileApps; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.Process; +import android.os.UserHandle; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowCrossProfileApps.StartedActivity; +import org.robolectric.shadows.ShadowCrossProfileApps.StartedMainActivity; + +@RunWith(AndroidJUnit4.class) +@Config(minSdk = P) +public class ShadowCrossProfileAppsTest { + + private final Application application = ApplicationProvider.getApplicationContext(); + private final CrossProfileApps crossProfileApps = + application.getSystemService(CrossProfileApps.class); + private final PackageManager packageManager = ((Context) application).getPackageManager(); + + private final UserHandle userHandle1 = UserHandle.of(10); + private final UserHandle userHandle2 = UserHandle.of(11); + + @Test + public void getTargetUserProfiles_noProfilesAdded_shouldReturnEmpty() { + assertThat(crossProfileApps.getTargetUserProfiles()).isEmpty(); + } + + @Test + public void getTargetUserProfiles_oneProfileAdded_shouldReturnProfileAdded() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + + assertThat(crossProfileApps.getTargetUserProfiles()).containsExactly(userHandle1); + } + + @Test + public void getTargetUserProfiles_oneProfileAddedTwice_shouldReturnSingleProfileAdded() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + + assertThat(crossProfileApps.getTargetUserProfiles()).containsExactly(userHandle1); + } + + @Test + public void getTargetUserProfiles_multipleProfilesAdded_shouldReturnAllProfilesAddedInOrder() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + shadowOf(crossProfileApps).addTargetUserProfile(userHandle2); + + assertThat(crossProfileApps.getTargetUserProfiles()) + .containsExactly(userHandle1, userHandle2) + .inOrder(); + } + + @Test + public void + getTargetUserProfiles_multipleProfilesAddedInAlternateOrder_shouldReturnAllProfilesAddedInOrder() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle2); + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + + assertThat(crossProfileApps.getTargetUserProfiles()) + .containsExactly(userHandle2, userHandle1) + .inOrder(); + } + + @Test + public void + getTargetUserProfiles_multipleProfilesAddedAndFirstRemoved_shouldReturnSecondProfile() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + shadowOf(crossProfileApps).addTargetUserProfile(userHandle2); + shadowOf(crossProfileApps).removeTargetUserProfile(userHandle1); + + assertThat(crossProfileApps.getTargetUserProfiles()).containsExactly(userHandle2); + } + + @Test + public void getTargetUserProfiles_multipleProfilesAddedAndCleared_shouldReturnEmpty() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + shadowOf(crossProfileApps).addTargetUserProfile(userHandle2); + shadowOf(crossProfileApps).clearTargetUserProfiles(); + + assertThat(crossProfileApps.getTargetUserProfiles()).isEmpty(); + } + + @Test + public void getProfileSwitchingLabel_shouldNotBeEmpty() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + + CharSequence label = crossProfileApps.getProfileSwitchingLabel(userHandle1); + assertThat(label.toString()).isNotEmpty(); + } + + @Test + public void getProfileSwitchingLabel_shouldBeDifferentForDifferentProfiles() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + shadowOf(crossProfileApps).addTargetUserProfile(userHandle2); + + assertThat(crossProfileApps.getProfileSwitchingLabel(userHandle1).toString()) + .isNotEqualTo(crossProfileApps.getProfileSwitchingLabel(userHandle2).toString()); + } + + @Test + public void getProfileSwitchingLabel_userNotAvailable_shouldThrowSecurityException() { + assertThrowsSecurityException(() -> crossProfileApps.getProfileSwitchingLabel(userHandle1)); + } + + @Test + public void getProfileSwitchingIconDrawable_shouldNotBeNull() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + + Drawable icon = crossProfileApps.getProfileSwitchingIconDrawable(userHandle1); + assertThat(icon).isNotNull(); + } + + @Test + public void getProfileSwitchingIconDrawable_shouldBeDifferentForDifferentProfiles() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + shadowOf(crossProfileApps).addTargetUserProfile(userHandle2); + + assertThat(crossProfileApps.getProfileSwitchingIconDrawable(userHandle1)) + .isNotEqualTo(crossProfileApps.getProfileSwitchingIconDrawable(userHandle2)); + } + + @Test + public void getProfileSwitchingIconDrawable_userNotAvailable_shouldThrowSecurityException() { + assertThrowsSecurityException( + () -> crossProfileApps.getProfileSwitchingIconDrawable(userHandle1)); + } + + @Test + public void startMainActivity_launcherActivityInManifest_shouldSucceed() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + + ComponentName component = + ComponentName.createRelative(application, ".shadows.TestActivityAlias"); + crossProfileApps.startMainActivity(component, userHandle1); + + StartedActivity startedActivity = shadowOf(crossProfileApps).peekNextStartedActivity(); + assertThat(startedActivity.getComponentName()).isEqualTo(component); + assertThat(startedActivity.getUserHandle()).isEqualTo(userHandle1); + assertThat(startedActivity).isEqualTo(new StartedActivity(component, userHandle1)); + } + + @Test + public void + startMainActivity_launcherActivityInManifest_withoutCrossProfilePermission_shouldSucceed() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + shadowOf(application).denyPermissions(INTERACT_ACROSS_PROFILES); + + ComponentName component = + ComponentName.createRelative(application, ".shadows.TestActivityAlias"); + crossProfileApps.startMainActivity(component, userHandle1); + + StartedActivity startedActivity = shadowOf(crossProfileApps).peekNextStartedActivity(); + assertThat(startedActivity.getComponentName()).isEqualTo(component); + assertThat(startedActivity.getUserHandle()).isEqualTo(userHandle1); + assertThat(startedActivity).isEqualTo(new StartedActivity(component, userHandle1)); + } + + @Test + public void startMainActivity_launcherActivityInManifest_shouldStillAddStartedMainActivity() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + + ComponentName component = + ComponentName.createRelative(application, ".shadows.TestActivityAlias"); + crossProfileApps.startMainActivity(component, userHandle1); + + StartedMainActivity startedMainActivity = + shadowOf(crossProfileApps).peekNextStartedMainActivity(); + assertThat(startedMainActivity.getComponentName()).isEqualTo(component); + assertThat(startedMainActivity.getUserHandle()).isEqualTo(userHandle1); + assertThat(startedMainActivity).isEqualTo(new StartedMainActivity(component, userHandle1)); + } + + @Test + public void startMainActivity_nonLauncherActivityInManifest_shouldThrowSecurityException() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + + ComponentName component = ComponentName.createRelative(application, ".shadows.TestActivity"); + assertThrowsSecurityException(() -> crossProfileApps.startMainActivity(component, userHandle1)); + } + + @Test + public void startMainActivity_nonExistentActivity_shouldThrowSecurityException() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + + ComponentName component = + ComponentName.createRelative(application, ".shadows.FakeTestActivity"); + assertThrowsSecurityException(() -> crossProfileApps.startMainActivity(component, userHandle1)); + } + + @Test + public void startMainActivity_userNotAvailable_shouldThrowSecurityException() { + ComponentName component = + ComponentName.createRelative(application, ".shadows.TestActivityAlias"); + assertThrowsSecurityException(() -> crossProfileApps.startMainActivity(component, userHandle1)); + } + + @Test + @Config(sdk = Q) + public void startActivity_launcherActivityInManifest_shouldSucceed() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + shadowOf(application).grantPermissions(INTERACT_ACROSS_PROFILES); + + ComponentName component = + ComponentName.createRelative(application, ".shadows.TestActivityAlias"); + crossProfileApps.startActivity(component, userHandle1); + + StartedActivity startedMainActivity = shadowOf(crossProfileApps).peekNextStartedActivity(); + assertThat(startedMainActivity.getComponentName()).isEqualTo(component); + assertThat(startedMainActivity.getUserHandle()).isEqualTo(userHandle1); + assertThat(startedMainActivity).isEqualTo(new StartedActivity(component, userHandle1)); + } + + @Test + @Config(sdk = Q) + public void startActivity_nonLauncherActivityInManifest_shouldSucceed() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + shadowOf(application).grantPermissions(INTERACT_ACROSS_PROFILES); + + ComponentName component = ComponentName.createRelative(application, ".shadows.TestActivity"); + + crossProfileApps.startActivity(component, userHandle1); + + StartedActivity startedMainActivity = shadowOf(crossProfileApps).peekNextStartedActivity(); + assertThat(startedMainActivity.getComponentName()).isEqualTo(component); + assertThat(startedMainActivity.getUserHandle()).isEqualTo(userHandle1); + assertThat(startedMainActivity).isEqualTo(new StartedActivity(component, userHandle1)); + } + + @Test + @Config(sdk = Q) + public void startActivity_nonExistentActivity_shouldThrowSecurityException() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + shadowOf(application).grantPermissions(INTERACT_ACROSS_PROFILES); + + ComponentName component = + ComponentName.createRelative(application, ".shadows.FakeTestActivity"); + assertThrowsSecurityException(() -> crossProfileApps.startActivity(component, userHandle1)); + } + + @Test + @Config(sdk = Q) + public void startActivity_userNotAvailable_shouldThrowSecurityException() { + shadowOf(application).grantPermissions(INTERACT_ACROSS_PROFILES); + + ComponentName component = + ComponentName.createRelative(application, ".shadows.TestActivityAlias"); + assertThrowsSecurityException(() -> crossProfileApps.startActivity(component, userHandle1)); + } + + @Test + @Config(sdk = Q) + public void startActivity_withoutPermission_shouldThrowSecurityException() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + shadowOf(application).denyPermissions(INTERACT_ACROSS_PROFILES); + + ComponentName component = ComponentName.createRelative(application, ".shadows.TestActivity"); + + assertThrowsSecurityException(() -> crossProfileApps.startActivity(component, userHandle1)); + } + + @Test + public void addTargetProfile_currentUserHandle_shouldThrowIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, + () -> shadowOf(crossProfileApps).addTargetUserProfile(Process.myUserHandle())); + } + + @Test + public void peekNextStartedActivity_activityStarted_shouldReturnAndNotConsumeActivity() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + ComponentName component = + ComponentName.createRelative(application, ".shadows.TestActivityAlias"); + crossProfileApps.startMainActivity(component, userHandle1); + + StartedActivity startedActivity = shadowOf(crossProfileApps).peekNextStartedActivity(); + + assertThat(startedActivity).isEqualTo(new StartedActivity(component, userHandle1)); + assertThat(shadowOf(crossProfileApps).peekNextStartedActivity()).isSameAs(startedActivity); + } + + @Test + public void peekNextStartedActivity_activityNotStarted_shouldReturnNull() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + + assertThat(shadowOf(crossProfileApps).peekNextStartedActivity()).isNull(); + } + + @Test + public void getNextStartedActivity_activityStarted_shouldReturnAndConsumeActivity() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + ComponentName component = + ComponentName.createRelative(application, ".shadows.TestActivityAlias"); + crossProfileApps.startMainActivity(component, userHandle1); + + StartedActivity startedActivity = shadowOf(crossProfileApps).getNextStartedActivity(); + + assertThat(startedActivity).isEqualTo(new StartedActivity(component, userHandle1)); + assertThat(shadowOf(crossProfileApps).peekNextStartedActivity()).isNull(); + } + + @Test + public void getNextStartedActivity_activityNotStarted_shouldReturnNull() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + + assertThat(shadowOf(crossProfileApps).getNextStartedActivity()).isNull(); + } + + @Test + public void + clearNextStartedActivities_activityStarted_shouldClearReferencesToStartedActivities() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + ComponentName component = + ComponentName.createRelative(application, ".shadows.TestActivityAlias"); + crossProfileApps.startMainActivity(component, userHandle1); + + shadowOf(crossProfileApps).clearNextStartedActivities(); + + assertThat(shadowOf(crossProfileApps).peekNextStartedActivity()).isNull(); + } + + @Test + public void clearNextStartedActivities_activityNotStarted_shouldBeNoOp() { + shadowOf(crossProfileApps).addTargetUserProfile(userHandle1); + + shadowOf(crossProfileApps).clearNextStartedActivities(); + + assertThat(shadowOf(crossProfileApps).peekNextStartedActivity()).isNull(); + } + + // BEGIN-INTERNAL + @Test + @Config(minSdk = R) + public void clearInteractAcrossProfilesAppOps_clearsAppOps() { + String testPackage1 = "com.example.testpackage1"; + String testPackage2 = "com.example.testpackage2"; + shadowOf(packageManager).installPackage(buildTestPackageInfo(testPackage1)); + shadowOf(packageManager).installPackage(buildTestPackageInfo(testPackage2)); + shadowOf(crossProfileApps).setInteractAcrossProfilesAppOp(testPackage1, MODE_ALLOWED); + shadowOf(crossProfileApps).setInteractAcrossProfilesAppOp(testPackage2, MODE_ALLOWED); + + shadowOf(crossProfileApps).clearInteractAcrossProfilesAppOps(); + + assertThat(shadowOf(crossProfileApps).getInteractAcrossProfilesAppOp(testPackage1)) + .isEqualTo(MODE_DEFAULT); + assertThat(shadowOf(crossProfileApps).getInteractAcrossProfilesAppOp(testPackage2)) + .isEqualTo(MODE_DEFAULT); + } + + private PackageInfo buildTestPackageInfo(String packageName) { + PackageInfo packageInfo = new PackageInfo(); + packageInfo.packageName = packageName; + packageInfo.applicationInfo = new ApplicationInfo(); + packageInfo.applicationInfo.packageName = packageName; + packageInfo.applicationInfo.name = "test"; + return packageInfo; + } + // END-INTERNAL + + private static void assertThrowsSecurityException(Runnable runnable) { + assertThrows(SecurityException.class, runnable); + } + + private static <T extends Throwable> void assertThrows(Class<T> clazz, Runnable runnable) { + try { + runnable.run(); + } catch (Throwable t) { + if (clazz.isInstance(t)) { + // expected + return; + } else { + fail("did not throw " + clazz.getName() + ", threw " + t + " instead"); + } + } + fail("did not throw " + clazz.getName()); + } +} diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java index dce3c7248..46a8688d3 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java @@ -97,6 +97,8 @@ import org.robolectric.R; import org.robolectric.Robolectric; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowPackageManager.PackageSetting; +import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.ReflectionHelpers.ClassParameter; @RunWith(AndroidJUnit4.class) public class ShadowPackageManagerTest { @@ -107,6 +109,7 @@ public class ShadowPackageManagerTest { private static final String TEST_PACKAGE2_NAME = "com.a.second.package"; private static final String TEST_PACKAGE2_LABEL = "A Second App"; private static final String TEST_APP2_PATH = "/values/app/application2.apk"; + private static final Object USER_ID = 1; protected ShadowPackageManager shadowPackageManager; @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -932,6 +935,43 @@ public class ShadowPackageManagerTest { } @Test + @Config(minSdk = JELLY_BEAN_MR1) + public void resolveActivityAsUser_Match() throws Exception { + Intent i = new Intent(Intent.ACTION_MAIN, null).addCategory(Intent.CATEGORY_LAUNCHER); + ResolveInfo info = new ResolveInfo(); + info.nonLocalizedLabel = TEST_PACKAGE_LABEL; + info.activityInfo = new ActivityInfo(); + info.activityInfo.name = "name"; + info.activityInfo.packageName = TEST_PACKAGE_NAME; + shadowPackageManager.addResolveInfoForIntent(i, info); + + ResolveInfo resolvedActivity = + ReflectionHelpers.callInstanceMethod(packageManager, "resolveActivityAsUser", + ClassParameter.from(Intent.class, i), + ClassParameter.from(int.class, 0), + ClassParameter.from(int.class, USER_ID)); + + assertThat(resolvedActivity).isNotNull(); + assertThat(resolvedActivity.activityInfo.name).isEqualTo("name"); + assertThat(resolvedActivity.activityInfo.packageName).isEqualTo(TEST_PACKAGE_NAME); + } + + @Test + @Config(minSdk = JELLY_BEAN_MR1) + public void resolveActivityAsUser_NoMatch() throws Exception { + Intent i = new Intent(); + i.setComponent(new ComponentName("foo.bar", "No Activity")); + + ResolveInfo resolvedActivity = + ReflectionHelpers.callInstanceMethod(packageManager, "resolveActivityAsUser", + ClassParameter.from(Intent.class, i), + ClassParameter.from(int.class, 0), + ClassParameter.from(int.class, USER_ID)); + + assertThat(resolvedActivity).isNull(); + } + + @Test public void queryIntentServices_EmptyResult() throws Exception { Intent i = new Intent(Intent.ACTION_MAIN, null); i.addCategory(Intent.CATEGORY_LAUNCHER); diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java index cd67c4c63..e05b8f868 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java @@ -34,6 +34,11 @@ public class ShadowUserManagerTest { private UserManager userManager; private Context context; + private static final int TEST_USER_HANDLE = 0; + private static final int PROFILE_USER_HANDLE = 2; + private static final String PROFILE_USER_NAME = "profile"; + private static final int PROFILE_USER_FLAGS = 0; + @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); @@ -131,6 +136,18 @@ public class ShadowUserManagerTest { } @Test + @Config(minSdk = N) + public void isManagedProfileWithHandle() { + shadowOf(userManager).addUser(TEST_USER_HANDLE, "secondary user", 0); + shadowOf(userManager).addProfile( + TEST_USER_HANDLE, + PROFILE_USER_HANDLE, + "another managed profile", + ShadowUserManager.FLAG_MANAGED_PROFILE); + assertThat(userManager.isManagedProfile(PROFILE_USER_HANDLE)).isTrue(); + } + + @Test @Config(minSdk = LOLLIPOP) public void enforcePermissionChecks() throws Exception { shadowOf(userManager).enforcePermissionChecks(true); @@ -335,6 +352,15 @@ public class ShadowUserManagerTest { } } + @Test + @Config(minSdk = LOLLIPOP) + public void getProfiles_addedProfile_containsProfile() { + shadowOf(userManager).addProfile( + TEST_USER_HANDLE, PROFILE_USER_HANDLE, PROFILE_USER_NAME, PROFILE_USER_FLAGS); + + assertThat(userManager.getProfiles(TEST_USER_HANDLE).get(0).id).isEqualTo(PROFILE_USER_HANDLE); + } + // Create user handle from parcel since UserHandle.of() was only added in later APIs. private static UserHandle newUserHandle(int uid) { Parcel userParcel = Parcel.obtain(); diff --git a/run_robolectric_module_tests.mk b/run_robolectric_module_tests.mk index 4d5cb95c5..43c93ae61 100644 --- a/run_robolectric_module_tests.mk +++ b/run_robolectric_module_tests.mk @@ -51,7 +51,8 @@ copy_android_all_jar_pairs := \ $(android_all_source_dir)/android-all-8.0.0_r4-robolectric-r1.jar:$(android_all_target_dir)/android-all-8.0.0_r4-robolectric-r1.jar \ $(android_all_source_dir)/android-all-8.1.0-robolectric-4611349.jar:$(android_all_target_dir)/android-all-8.1.0-robolectric-4611349.jar \ $(android_all_source_dir)/android-all-9-robolectric-4913185-2.jar:$(android_all_target_dir)/android-all-9-robolectric-4913185-2.jar \ - $(local_android_all_source_jar):$(android_all_target_dir)/android-all-Q-robolectric-r0.jar + $(android_all_source_dir)/android-all-9plus-robolectric-5616371.jar:$(android_all_target_dir)/android-all-9plus-robolectric-5616371.jar \ + $(local_android_all_source_jar):$(android_all_target_dir)/android-all-R-robolectric-r0.jar copy_android_all_jars := $(call copy-many-files, $(copy_android_all_jar_pairs)) # If debugging the tests was requested, set up the JVM parameters to enable it. diff --git a/shadows/framework/src/main/java/org/robolectric/android/internal/DisplayConfig.java b/shadows/framework/src/main/java/org/robolectric/android/internal/DisplayConfig.java index 463c8170f..f6bf1bc6a 100644 --- a/shadows/framework/src/main/java/org/robolectric/android/internal/DisplayConfig.java +++ b/shadows/framework/src/main/java/org/robolectric/android/internal/DisplayConfig.java @@ -126,30 +126,6 @@ public final class DisplayConfig { public int logicalHeight; /** - * @hide - * Number of overscan pixels on the left side of the display. - */ - public int overscanLeft; - - /** - * @hide - * Number of overscan pixels on the top side of the display. - */ - public int overscanTop; - - /** - * @hide - * Number of overscan pixels on the right side of the display. - */ - public int overscanRight; - - /** - * @hide - * Number of overscan pixels on the bottom side of the display. - */ - public int overscanBottom; - - /** * The rotation of the display relative to its natural orientation. * May be one of {@link Surface#ROTATION_0}, * {@link Surface#ROTATION_90}, {@link Surface#ROTATION_180}, @@ -281,12 +257,6 @@ public final class DisplayConfig { largestNominalAppHeight = other.largestNominalAppHeight; logicalWidth = other.logicalWidth; logicalHeight = other.logicalHeight; - if (RuntimeEnvironment.getApiLevel() >= JELLY_BEAN_MR2) { - overscanLeft = other.overscanLeft; - overscanTop = other.overscanTop; - overscanRight = other.overscanRight; - overscanBottom = other.overscanBottom; - } rotation = other.rotation; if (RuntimeEnvironment.getApiLevel() >= M) { modeId = other.modeId; @@ -339,10 +309,6 @@ public final class DisplayConfig { && largestNominalAppHeight == other.largestNominalAppHeight && logicalWidth == other.logicalWidth && logicalHeight == other.logicalHeight - && overscanLeft == other.overscanLeft - && overscanTop == other.overscanTop - && overscanRight == other.overscanRight - && overscanBottom == other.overscanBottom && rotation == other.rotation && modeId == other.modeId && defaultModeId == other.defaultModeId @@ -380,10 +346,6 @@ public final class DisplayConfig { largestNominalAppHeight = other.largestNominalAppHeight; logicalWidth = other.logicalWidth; logicalHeight = other.logicalHeight; - overscanLeft = other.overscanLeft; - overscanTop = other.overscanTop; - overscanRight = other.overscanRight; - overscanBottom = other.overscanBottom; rotation = other.rotation; modeId = other.modeId; defaultModeId = other.defaultModeId; @@ -422,12 +384,6 @@ public final class DisplayConfig { other.largestNominalAppHeight = largestNominalAppHeight; other.logicalWidth = logicalWidth; other.logicalHeight = logicalHeight; - if (RuntimeEnvironment.getApiLevel() >= JELLY_BEAN_MR2) { - other.overscanLeft = overscanLeft; - other.overscanTop = overscanTop; - other.overscanRight = overscanRight; - other.overscanBottom = overscanBottom; - } other.rotation = rotation; if (RuntimeEnvironment.getApiLevel() >= M) { other.modeId = modeId; @@ -475,17 +431,6 @@ public final class DisplayConfig { sb.append(logicalWidth); sb.append(" x "); sb.append(logicalHeight); - if (overscanLeft != 0 || overscanTop != 0 || overscanRight != 0 || overscanBottom != 0) { - sb.append(", overscan ("); - sb.append(overscanLeft); - sb.append(","); - sb.append(overscanTop); - sb.append(","); - sb.append(overscanRight); - sb.append(","); - sb.append(overscanBottom); - sb.append(")"); - } sb.append(", largest app "); sb.append(largestNominalAppWidth); sb.append(" x "); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java index 29c9a8519..f41493e1a 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java @@ -1,14 +1,20 @@ package org.robolectric.shadows; +import static android.os.Build.VERSION_CODES.R; + import android.app.ActivityThread; import android.content.ComponentName; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; import android.os.RemoteException; +import android.os.UserHandle; + import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Collections; + import javax.annotation.Nonnull; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; @@ -71,18 +77,51 @@ public class ShadowActivityThread { } catch (PackageManager.NameNotFoundException e) { throw new RemoteException(e.getMessage()); } + } else if (method.getName().equals("getInstalledApplications")) { + int flags = (Integer) args[0]; + int userId = (Integer) args[1]; + return new ParceledListSlice<>( + RuntimeEnvironment.application + .getApplicationContext() + .createContextAsUser(UserHandle.of(userId), /* flags= */ 0) + .getPackageManager() + .getInstalledApplications(flags)); } else if (method.getName().equals("notifyPackageUse")) { return null; } else if (method.getName().equals("getPackageInstaller")) { return null; - } else if (method.getName().equals("getSplitPermissions")) { - return Collections.emptyList(); } throw new UnsupportedOperationException("sorry, not supporting " + method + " yet!"); } }); } + // BEGIN-INTERNAL + @Implementation(minSdk = R) + public static Object getPermissionManager() { + ClassLoader classLoader = ShadowActivityThread.class.getClassLoader(); + Class<?> iPermissionManagerClass; + try { + iPermissionManagerClass = classLoader.loadClass("android.permission.IPermissionManager"); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + return Proxy.newProxyInstance( + classLoader, + new Class[] {iPermissionManagerClass}, + new InvocationHandler() { + @Override + public Object invoke(Object proxy, @Nonnull Method method, Object[] args) + throws Exception { + if (method.getName().equals("getSplitPermissions")) { + return Collections.emptyList(); + } + return method.getDefaultValue(); + } + }); + } + // END-INTERNAL + @Implementation public static Object currentActivityThread() { return RuntimeEnvironment.getActivityThread(); diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAnnotationValidations.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAnnotationValidations.java new file mode 100644 index 000000000..45f3a79e2 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAnnotationValidations.java @@ -0,0 +1,19 @@ +package org.robolectric.shadows; + +import java.lang.annotation.Annotation; +import com.android.internal.util.AnnotationValidations; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +@Implements(value = AnnotationValidations.class) +public class ShadowAnnotationValidations { + @Implementation + public static void validate(Class<? extends Annotation> annotation, + Annotation ignored, int value) { + } + + @Implementation + public static void validate(Class<? extends Annotation> annotation, + Annotation ignored, long value) { + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppOpsManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppOpsManager.java index c250dfcd9..04a2f79f7 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppOpsManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppOpsManager.java @@ -6,6 +6,7 @@ import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.P; // BEGIN-INTERNAL import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; // END-INTERNAL import static org.robolectric.shadow.api.Shadow.invokeConstructor; @@ -13,6 +14,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.app.AppOpsManager; +import android.app.AppOpsManager.AttributedOpEntry; import android.app.AppOpsManager.OnOpChangedListener; import android.app.AppOpsManager.OpEntry; import android.app.AppOpsManager.PackageOps; @@ -23,6 +25,7 @@ import android.os.Binder; import android.os.Build; import android.util.LongSparseArray; import android.util.LongSparseLongArray; + import com.android.internal.app.IAppOpsService; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; @@ -129,6 +132,24 @@ public class ShadowAppOpsManager { } // END-INTERNAL + /** + * Like {@link #unsafeCheckOpNoThrow(String, int, String)} but returns the <em>raw</em> mode + * associated with the op. Does not throw a security exception, does not translate {@link + * AppOpsManager#MODE_FOREGROUND}. + */ + @Implementation(minSdk = Q) + protected int unsafeCheckOpRawNoThrow(String op, int uid, String packageName) { + return unsafeCheckOpRawNoThrow(AppOpsManager.strOpToOp(op), uid, packageName); + } + + private int unsafeCheckOpRawNoThrow(int op, int uid, String packageName) { + Integer mode = appModeMap.get(getOpMapKey(uid, packageName, op)); + if (mode == null) { + return AppOpsManager.MODE_ALLOWED; + } + return mode; + } + @Implementation(minSdk = P) @Deprecated // renamed to unsafeCheckOpNoThrow protected int checkOpNoThrow(String op, int uid, String packageName) { @@ -151,7 +172,7 @@ public class ShadowAppOpsManager { return mode; } - @Implementation(minSdk = KITKAT) + @Implementation(minSdk = KITKAT, maxSdk = Q) public int noteOp(int op, int uid, String packageName) { mStoredOps.put(getInternalKey(uid, packageName), op); @@ -159,13 +180,33 @@ public class ShadowAppOpsManager { return AppOpsManager.MODE_ALLOWED; } - @Implementation(minSdk = M) + @Implementation(minSdk = R) + @HiddenApi + public int noteOp(int op, int uid, String packageName, String message) { + mStoredOps.put(getInternalKey(uid, packageName), op); + + // Permission check not currently implemented in this shadow. + return AppOpsManager.MODE_ALLOWED; + } + + @Implementation(minSdk = M, maxSdk = Q) @HiddenApi protected int noteProxyOpNoThrow(int op, String proxiedPackageName) { mStoredOps.put(getInternalKey(Binder.getCallingUid(), proxiedPackageName), op); return checkOpNoThrow(op, Binder.getCallingUid(), proxiedPackageName); } + @Implementation(minSdk = R) + @HiddenApi + protected int noteProxyOpNoThrow(int op, String proxiedPackageName, int proxiedUid, + String featureId, String message) { + if (featureId != null) { + throw new RuntimeException("non null featureIds are not supported by Robolectric yet"); + } + mStoredOps.put(getInternalKey(proxiedUid, proxiedPackageName), op); + return checkOpNoThrow(op, proxiedUid, proxiedPackageName); + } + @Implementation(minSdk = KITKAT) @HiddenApi public List<PackageOps> getOpsForPackage(int uid, String packageName, int[] ops) { @@ -274,8 +315,28 @@ public class ShadowAppOpsManager { final LongSparseArray<String> proxyPackages = new LongSparseArray<>(); proxyPackages.put(key, PROXY_PACKAGE); - return new OpEntry(op, false, AppOpsManager.MODE_ALLOWED, accessTimes, - durations, rejectTimes, proxyUids, proxyPackages); + if (RuntimeEnvironment.getApiLevel() <= Build.VERSION_CODES.Q) { + return ReflectionHelpers.callConstructor( + OpEntry.class, + ClassParameter.from(int.class, op), + ClassParameter.from(boolean.class, false), + ClassParameter.from(int.class, AppOpsManager.MODE_ALLOWED), + ClassParameter.from(LongSparseLongArray.class, accessTimes), + ClassParameter.from(LongSparseLongArray.class, durations), + ClassParameter.from(LongSparseLongArray.class, rejectTimes), + ClassParameter.from(LongSparseLongArray.class, proxyUids), + ClassParameter.from(LongSparseArray.class, proxyPackages)); + } + + LongSparseArray<AppOpsManager.NoteOpEvent> accessEvents = new LongSparseArray<>(); + LongSparseArray<AppOpsManager.NoteOpEvent> rejectEvents = new LongSparseArray<>(); + + accessEvents.put(key, new AppOpsManager.NoteOpEvent(OP_TIME, DURATION, + new AppOpsManager.OpEventProxyInfo(PROXY_UID, PROXY_PACKAGE, null))); + rejectEvents.put(key, new AppOpsManager.NoteOpEvent(REJECT_TIME, -1, null)); + + return new OpEntry(op, AppOpsManager.MODE_ALLOWED, Collections.singletonMap(null, + new AttributedOpEntry(op, false, accessEvents, rejectEvents))); // END-INTERNAL } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java index 821027bec..118d2f64e 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java @@ -21,6 +21,8 @@ import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.O_MR1; import static android.os.Build.VERSION_CODES.P; import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; + import static org.robolectric.shadow.api.Shadow.invokeConstructor; import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; @@ -79,6 +81,7 @@ import android.os.RemoteException; import android.os.UserHandle; import android.os.storage.VolumeInfo; +import android.permission.IPermissionManager; import android.telecom.TelecomManager; import android.util.Pair; import com.google.common.base.Function; @@ -115,7 +118,7 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager { private Context context; - @Implementation + @Implementation(maxSdk = Q) protected void __constructor__(Object contextImpl, Object pm) { try { invokeConstructor( @@ -129,6 +132,24 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager { context = (Context) contextImpl; } + // BEGIN-INTERNAL + @Implementation(minSdk = R) + protected void __constructor__( + Object contextImpl, Object packageManager, Object permissionManager) { + try { + invokeConstructor( + ApplicationPackageManager.class, + realObject, + from(Class.forName(ShadowContextImpl.CLASS_NAME), contextImpl), + from(IPackageManager.class, packageManager), + from(IPermissionManager.class, permissionManager)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + context = (Context) contextImpl; + } + // END-INTERNAL + @Implementation public List<PackageInfo> getInstalledPackages(int flags) { List<PackageInfo> result = new ArrayList<>(); @@ -999,8 +1020,8 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager { } @Implementation - protected Drawable getApplicationIcon(ApplicationInfo info) { - return null; + protected Drawable getApplicationIcon(ApplicationInfo info) throws NameNotFoundException { + return getApplicationIcon(info.packageName); } @Implementation(minSdk = LOLLIPOP) @@ -1086,7 +1107,7 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager { @Implementation(minSdk = N) protected PackageInfo getPackageInfoAsUser(String packageName, int flags, int userId) throws NameNotFoundException { - return null; + return getPackageInfo(packageName, flags); } @Implementation @@ -1198,6 +1219,13 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager { throw new NameNotFoundException(packageName); } + @Implementation + protected ApplicationInfo getApplicationInfoAsUser( + String packageName, int flags, UserHandle userId) throws NameNotFoundException { + // Currently does not use the user ID. + return getApplicationInfo(packageName, flags); + } + /** * Returns all the values added via {@link * ShadowPackageManager#addSystemSharedLibraryName(String)}. @@ -1320,7 +1348,11 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager { @Implementation(minSdk = N) protected List<PackageInfo> getInstalledPackagesAsUser(int flags, int userId) { - return null; + List<PackageInfo> packages = new ArrayList<>(); + for (String packageName : packagesForUserId.getOrDefault(userId, new ArrayList<>())) { + packages.add(packageInfos.get(packageName)); + } + return packages; } @Implementation(minSdk = JELLY_BEAN_MR2) @@ -1328,9 +1360,10 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager { return null; } + /** Behaves as {@link #resolveActivity(Intent, int)} and currently ignores userId. */ @Implementation(minSdk = JELLY_BEAN_MR1) protected ResolveInfo resolveActivityAsUser(Intent intent, int flags, int userId) { - return null; + return resolveActivity(intent, flags); } @Implementation diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java index 109c191c9..2b9be09fa 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java @@ -8,13 +8,9 @@ import static org.robolectric.shadow.api.Shadow.directlyOn; import android.annotation.NonNull; import android.content.res.ApkAssets; import android.content.res.AssetManager; +import android.content.res.loader.AssetsProvider; import android.os.Build; -import java.io.FileDescriptor; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.util.HashMap; -import java.util.Objects; + import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -28,6 +24,13 @@ import org.robolectric.shadows.ShadowApkAssets.Picker; import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.ReflectionHelpers.ClassParameter; +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Objects; + // transliterated from // https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/core/jni/android_content_res_ApkAssets.cpp @@ -51,6 +54,12 @@ public class ShadowArscApkAssets9 extends ShadowApkAssets { // using ::android::base::unique_fd; // // namespace android { + // BEGIN-INTERNAL + private static final int PROPERTY_SYSTEM = 1 << 0; + private static final int PROPERTY_DYNAMIC = 1 << 1; + private static final int PROPERTY_LOADER = 1 << 2; + private static final int PROPERTY_OVERLAY = 1 << 3; + // END-INTERNAL private static final String FRAMEWORK_APK_PATH = ReflectionHelpers.getStaticField(AssetManager.class, "FRAMEWORK_APK_PATH"); @@ -167,6 +176,25 @@ public class ShadowArscApkAssets9 extends ShadowApkAssets { ClassParameter.from(boolean.class, system))); } + // BEGIN-INTERNAL + @Implementation(minSdk = Build.VERSION_CODES.R) + protected static ApkAssets loadFromPath(String path, int flags) + throws IOException { + boolean system = (flags & PROPERTY_SYSTEM) == PROPERTY_SYSTEM; + + if (FRAMEWORK_APK_PATH.equals(path)) { + path = RuntimeEnvironment.getAndroidFrameworkJarPath(); + } + + String finalPath = path; + return getFromCacheOrLoad( + new Key(null, path, system, false, false), + () -> directlyOn(ApkAssets.class, "loadFromPath", + ClassParameter.from(String.class, finalPath), + ClassParameter.from(int.class, flags))); + } + // END-INTERNAL + @Implementation protected static @NonNull ApkAssets loadFromPath(@NonNull String path, boolean system, boolean forceSharedLibrary) throws IOException { @@ -178,19 +206,51 @@ public class ShadowArscApkAssets9 extends ShadowApkAssets { ClassParameter.from(boolean.class, forceSharedLibrary))); } + // BEGIN-INTERNAL + @Implementation(minSdk = Build.VERSION_CODES.R) + protected static @NonNull ApkAssets loadFromPath(@NonNull String path, int flags, + AssetsProvider assets) throws IOException { + boolean system = (flags & PROPERTY_SYSTEM) == PROPERTY_SYSTEM; + boolean forceSharedLibrary = (flags & PROPERTY_DYNAMIC) == PROPERTY_DYNAMIC; + return getFromCacheOrLoad( + new Key(null, path, system, forceSharedLibrary, false), + () -> directlyOn(ApkAssets.class, "loadFromPath", + ClassParameter.from(String.class, path), + ClassParameter.from(int.class, flags), + ClassParameter.from(AssetsProvider.class, assets))); + } + // END-INTERNAL + @Implementation protected static ApkAssets loadFromFd(FileDescriptor fd, String friendlyName, boolean system, boolean forceSharedLibrary) throws IOException { return getFromCacheOrLoad( new Key(fd, friendlyName, system, forceSharedLibrary, false), - () -> directlyOn(ApkAssets.class, "loadFromPath", + () -> directlyOn(ApkAssets.class, "loadFromFd", ClassParameter.from(FileDescriptor.class, fd), ClassParameter.from(String.class, friendlyName), ClassParameter.from(boolean.class, system), ClassParameter.from(boolean.class, forceSharedLibrary))); } + // BEGIN-INTERNAL + @Implementation(minSdk = Build.VERSION_CODES.R) + protected static ApkAssets loadFromFd(FileDescriptor fd, + String friendlyName, int flags, AssetsProvider assets) + throws IOException { + boolean system = (flags & PROPERTY_SYSTEM) == PROPERTY_SYSTEM; + boolean forceSharedLibrary = (flags & PROPERTY_DYNAMIC) == PROPERTY_DYNAMIC; + return getFromCacheOrLoad( + new Key(fd, friendlyName, system, forceSharedLibrary, false), + () -> directlyOn(ApkAssets.class, "loadFromFd", + ClassParameter.from(FileDescriptor.class, fd), + ClassParameter.from(String.class, friendlyName), + ClassParameter.from(int.class, flags), + ClassParameter.from(AssetsProvider.class, assets))); + } + // END-INTERNAL + @Implementation protected static @NonNull ApkAssets loadOverlayFromPath(@NonNull String idmapPath, boolean system) throws IOException { @@ -206,7 +266,7 @@ public class ShadowArscApkAssets9 extends ShadowApkAssets { // static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, jstring java_path, jboolean system, // jboolean force_shared_lib, jboolean overlay) { - @Implementation + @Implementation(maxSdk = Build.VERSION_CODES.Q) protected static long nativeLoad(String java_path, boolean system, boolean force_shared_lib, boolean overlay) throws IOException { String path = java_path; @@ -239,9 +299,50 @@ public class ShadowArscApkAssets9 extends ShadowApkAssets { return Registries.NATIVE_APK_ASSETS_REGISTRY.register(apk_assets); } + // BEGIN-INTERNAL + // static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, const format_type_t format, + // jstring java_path, const jint property_flags, jobject assets_provider) + @Implementation(minSdk = Build.VERSION_CODES.R) + protected static long nativeLoad(int format, String java_path, + int flags, AssetsProvider asset) throws IOException { + String path = java_path; + if (path == null) { + return 0; + } + + ATRACE_NAME(String.format("LoadApkAssets(%s)", path)); + + boolean system = (flags & PROPERTY_SYSTEM) == PROPERTY_SYSTEM; + boolean overlay = (flags & PROPERTY_OVERLAY) == PROPERTY_OVERLAY; + boolean force_shared_lib = (flags & PROPERTY_DYNAMIC) == PROPERTY_DYNAMIC; + CppApkAssets apk_assets; + try { + if (overlay) { + apk_assets = CppApkAssets.LoadOverlay(path, system); + } else if (force_shared_lib) { + apk_assets = + CppApkAssets.LoadAsSharedLibrary(path, system); + } else { + apk_assets = CppApkAssets.Load(path, system); + } + } catch (OutOfMemoryError e) { + OutOfMemoryError outOfMemoryError = new OutOfMemoryError("Failed to load " + path); + outOfMemoryError.initCause(e); + throw outOfMemoryError; + } + + if (apk_assets == null) { + String error_msg = String.format("Failed to load asset path %s", path); + throw new IOException(error_msg); + } + return Registries.NATIVE_APK_ASSETS_REGISTRY.register(apk_assets); + } + // END-INTERNAL + // static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, jobject file_descriptor, -// jstring friendly_name, jboolean system, jboolean force_shared_lib) { - @Implementation +// jstring friendly_name, jboolean system, jboolean +// force_shared_lib) { + @Implementation(maxSdk = Build.VERSION_CODES.Q) protected static long nativeLoadFromFd(FileDescriptor file_descriptor, String friendly_name, boolean system, boolean force_shared_lib) { String friendly_name_utf8 = friendly_name; @@ -275,6 +376,45 @@ public class ShadowArscApkAssets9 extends ShadowApkAssets { // return ShadowArscAssetManager9.NATIVE_APK_ASSETS_REGISTRY.getNativeObjectId(apk_assets); } + // BEGIN-INTERNAL + // static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, const format_type_t format, + // jobject file_descriptor, jstring friendly_name, + // const jint property_flags, jobject assets_provider) { + @Implementation(minSdk = Build.VERSION_CODES.R) + protected static long nativeLoadFromFd(int format, FileDescriptor fd, + String friendlyName, int flags, AssetsProvider asset) { + String friendly_name_utf8 = friendlyName; + if (friendly_name_utf8 == null) { + return 0; + } + + throw new UnsupportedOperationException(); + // ATRACE_NAME(String.format("LoadApkAssetsFd(%s)", friendly_name_utf8)); + // + // int fd = jniGetFDFromFileDescriptor(env, file_descriptor); + // if (fd < 0) { + // throw new IllegalArgumentException("Bad FileDescriptor"); + // } + // + // unique_fd dup_fd(.dup(fd)); + // if (dup_fd < 0) { + // throw new IOException(errno); + // return 0; + // } + // + // ApkAssets apk_assets = ApkAssets.LoadFromFd(std.move(dup_fd), + // friendly_name_utf8, + // system, force_shared_lib); + // if (apk_assets == null) { + // String error_msg = String.format("Failed to load asset path %s from fd %d", + // friendly_name_utf8, dup_fd.get()); + // throw new IOException(error_msg); + // return 0; + // } + // return ShadowArscAssetManager9.NATIVE_APK_ASSETS_REGISTRY.getNativeObjectId(apk_assets); + } + // END-INTERNAL + // static jstring NativeGetAssetPath(JNIEnv* env, jclass /*clazz*/, jlong ptr) { @Implementation protected static String nativeGetAssetPath(long ptr) { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContextImpl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContextImpl.java index c6c2747a5..2b29bdb4d 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContextImpl.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContextImpl.java @@ -4,6 +4,7 @@ import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.N; import static android.os.Build.VERSION_CODES.O; import static org.robolectric.shadow.api.Shadow.directlyOn; @@ -179,6 +180,52 @@ public class ShadowContextImpl { realContextImpl); } + /** Behaves as {@link #sendOrderedBroadcast} and currently ignores userHandle. */ + @Implementation(minSdk = KITKAT) + protected void sendOrderedBroadcastAsUser( + Intent intent, + UserHandle userHandle, + String receiverPermission, + BroadcastReceiver resultReceiver, + Handler scheduler, + int initialCode, + String initialData, + Bundle initialExtras) { + sendOrderedBroadcast( + intent, + receiverPermission, + resultReceiver, + scheduler, + initialCode, + initialData, + initialExtras + ); + } + + /** Behaves as {@link #sendOrderedBroadcast}. Currently ignores userHandle, appOp, and options. */ + @Implementation(minSdk = M) + protected void sendOrderedBroadcastAsUser( + Intent intent, + UserHandle userHandle, + String receiverPermission, + int appOp, + Bundle options, + BroadcastReceiver resultReceiver, + Handler scheduler, + int initialCode, + String initialData, + Bundle initialExtras) { + sendOrderedBroadcast( + intent, + receiverPermission, + resultReceiver, + scheduler, + initialCode, + initialData, + initialExtras + ); + } + @Implementation protected void sendStickyBroadcast(Intent intent) { getShadowInstrumentation().sendStickyBroadcast(intent, realContextImpl); @@ -253,6 +300,22 @@ public class ShadowContextImpl { getShadowInstrumentation().unbindService(serviceConnection); } + /** + * Behaves as {@link #startActivity}. The user parameter is ignored. + */ + @Implementation(minSdk = LOLLIPOP) + protected void startActivityAsUser(Intent intent, Bundle options, UserHandle user) { + // TODO: Remove this once {@link com.android.server.wmActivityTaskManagerService} is + // properly shadowed. + directlyOn( + realContextImpl, + ShadowContextImpl.CLASS_NAME, + "startActivity", + ClassParameter.from(Intent.class, intent), + ClassParameter.from(Bundle.class, options) + ); + } + @Implementation(minSdk = JELLY_BEAN_MR1) protected int getUserId() { return 0; diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCrossProfileApps.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCrossProfileApps.java new file mode 100644 index 000000000..66ebe84eb --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCrossProfileApps.java @@ -0,0 +1,456 @@ +/* + * 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 org.robolectric.shadows; + +import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; +import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; +import static android.os.Build.VERSION_CODES.P; +import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; + +import static com.google.common.base.Preconditions.checkNotNull; + +import android.Manifest; +import android.Manifest.permission; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.app.AppOpsManager; +import android.app.AppOpsManager.Mode; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.CrossProfileApps; +import android.content.pm.ICrossProfileApps; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Process; +import android.os.UserHandle; +import android.text.TextUtils; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +/** Robolectric implementation of {@link CrossProfileApps}. */ +@Implements(value = CrossProfileApps.class, minSdk = P) +public class ShadowCrossProfileApps { + + // BEGIN-INTERNAL + private final static String INTERACT_ACROSS_PROFILES_APPOP = AppOpsManager.permissionToOp( + Manifest.permission.INTERACT_ACROSS_PROFILES); + private static final Set<String> configurableInteractAcrossProfilePackages = new HashSet<>(); + // END-INTERNAL + + private final Set<UserHandle> targetUserProfiles = new LinkedHashSet<>(); + private final List<StartedMainActivity> startedMainActivities = new ArrayList<>(); + private final List<StartedActivity> startedActivities = + Collections.synchronizedList(new ArrayList<>()); + private final Map<String, Integer> packageNameAppOpModes = new HashMap<>(); + + private Context context; + private PackageManager packageManager; + + @Implementation + protected void __constructor__(Context context, ICrossProfileApps service) { + this.context = context; + this.packageManager = context.getPackageManager(); + } + + /** + * Returns a list of {@link UserHandle}s currently accessible. This list is populated from calls + * to {@link #addTargetUserProfile(UserHandle)}. + */ + @Implementation + protected List<UserHandle> getTargetUserProfiles() { + return ImmutableList.copyOf(targetUserProfiles); + } + + /** + * Returns a {@link Drawable} that can be shown for profile switching, which is guaranteed to + * always be the same for a particular user and to be distinct between users. + */ + @Implementation + protected Drawable getProfileSwitchingIconDrawable(UserHandle userHandle) { + verifyCanAccessUser(userHandle); + return new ColorDrawable(userHandle.getIdentifier()); + } + + /** + * Returns a {@link CharSequence} that can be shown as a label for profile switching, which is + * guaranteed to always be the same for a particular user and to be distinct between users. + */ + @Implementation + protected CharSequence getProfileSwitchingLabel(UserHandle userHandle) { + verifyCanAccessUser(userHandle); + return "Switch to " + userHandle; + } + + /** + * Simulates starting the main activity specified in the specified profile, performing the same + * security checks done by the real {@link CrossProfileApps}. + * + * <p>The most recent main activity started can be queried by {@link #peekNextStartedActivity()} + * ()}. + */ + @Implementation + protected void startMainActivity(ComponentName componentName, UserHandle targetUser) { + verifyCanAccessUser(targetUser); + verifyActivityInManifest(componentName, /* requireMainActivity= */ true); + startedMainActivities.add(new StartedMainActivity(componentName, targetUser)); + startedActivities.add(new StartedActivity(componentName, targetUser)); + } + + /** + * Simulates starting the activity specified in the specified profile, performing the same + * security checks done by the real {@link CrossProfileApps}. + * + * <p>The most recent main activity started can be queried by {@link #peekNextStartedActivity()} + * ()}. + */ + @Implementation(minSdk = Q) + @SystemApi + @RequiresPermission(permission.INTERACT_ACROSS_PROFILES) + protected void startActivity(ComponentName componentName, UserHandle targetUser) { + verifyCanAccessUser(targetUser); + verifyActivityInManifest(componentName, /* requireMainActivity= */ false); + verifyHasInteractAcrossProfilesPermission(); + startedActivities.add(new StartedActivity(componentName, targetUser)); + } + + /** Adds {@code userHandle} to the list of accessible handles. */ + public void addTargetUserProfile(UserHandle userHandle) { + if (userHandle.equals(Process.myUserHandle())) { + throw new IllegalArgumentException("Cannot target current user"); + } + targetUserProfiles.add(userHandle); + } + + /** Removes {@code userHandle} from the list of accessible handles, if present. */ + public void removeTargetUserProfile(UserHandle userHandle) { + if (userHandle.equals(Process.myUserHandle())) { + throw new IllegalArgumentException("Cannot target current user"); + } + targetUserProfiles.remove(userHandle); + } + + /** Clears the list of accessible handles. */ + public void clearTargetUserProfiles() { + targetUserProfiles.clear(); + } + + /** + * Returns the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link + * CrossProfileApps#startMainActivity(ComponentName, UserHandle)}, wrapped in {@link + * StartedMainActivity}. + * + * @deprecated Use {@link #peekNextStartedActivity()} instead. + */ + @Nullable + @Deprecated + public StartedMainActivity peekNextStartedMainActivity() { + if (startedMainActivities.isEmpty()) { + return null; + } else { + return Iterables.getLast(startedMainActivities); + } + } + + /** + * Returns the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link + * CrossProfileApps#startMainActivity(ComponentName, UserHandle)} or {@link + * CrossProfileApps#startActivity(ComponentName, UserHandle)}, wrapped in {@link StartedActivity}. + */ + @Nullable + public StartedActivity peekNextStartedActivity() { + if (startedActivities.isEmpty()) { + return null; + } else { + return Iterables.getLast(startedActivities); + } + } + + /** + * Consumes the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link + * CrossProfileApps#startMainActivity(ComponentName, UserHandle)} or {@link + * CrossProfileApps#startActivity(ComponentName, UserHandle)}, and returns it wrapped in {@link + * StartedActivity}. + */ + @Nullable + public StartedActivity getNextStartedActivity() { + if (startedActivities.isEmpty()) { + return null; + } else { + return startedActivities.remove(startedActivities.size() - 1); + } + } + + /** + * Clears all records of {@link StartedActivity}s from calls to {@link + * CrossProfileApps#startActivity(ComponentName, UserHandle)} or {@link + * CrossProfileApps#startMainActivity(ComponentName, UserHandle)}. + */ + public void clearNextStartedActivities() { + startedActivities.clear(); + } + + private void verifyCanAccessUser(UserHandle userHandle) { + if (!targetUserProfiles.contains(userHandle)) { + throw new SecurityException( + "Not allowed to access " + + userHandle + + " (did you forget to call addTargetUserProfile?)"); + } + } + + private void verifyHasInteractAcrossProfilesPermission() { + if (context.checkSelfPermission(permission.INTERACT_ACROSS_PROFILES) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException( + "Attempt to launch activity without required " + + permission.INTERACT_ACROSS_PROFILES + + " permission"); + } + } + + /** + * Ensures that {@code component} is present in the manifest as an exported and enabled activity. + * This check and the error thrown are the same as the check done by the real {@link + * CrossProfileApps}. + * + * <p>If {@code requireMainActivity} is true, then this also asserts that the activity is a + * launcher activity. + */ + private void verifyActivityInManifest(ComponentName component, boolean requireMainActivity) { + Intent launchIntent = new Intent(); + if (requireMainActivity) { + launchIntent + .setAction(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_LAUNCHER) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + .setPackage(component.getPackageName()); + } else { + launchIntent.setComponent(component); + } + + boolean existsMatchingActivity = + Iterables.any( + packageManager.queryIntentActivities( + launchIntent, MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE), + resolveInfo -> { + ActivityInfo activityInfo = resolveInfo.activityInfo; + return TextUtils.equals(activityInfo.packageName, component.getPackageName()) + && TextUtils.equals(activityInfo.name, component.getClassName()) + && activityInfo.exported; + }); + if (!existsMatchingActivity) { + throw new SecurityException( + "Attempt to launch activity without " + + " category Intent.CATEGORY_LAUNCHER or activity is not exported" + + component); + } + } + + // BEGIN-INTERNAL + @Implementation(minSdk = R) + @RequiresPermission( + allOf={android.Manifest.permission.MANAGE_APP_OPS_MODES, + android.Manifest.permission.INTERACT_ACROSS_USERS}) + protected void setInteractAcrossProfilesAppOp(String packageName, @Mode int newMode) { + packageNameAppOpModes.put(packageName, newMode); + } + + /** + * Returns the app-op mode associated with the given package name. If not set, returns {@code + * null}. + */ + @Nullable + public @Mode Integer getInteractAcrossProfilesAppOp(String packageName) { + return packageNameAppOpModes.get(packageName); + } + + public void addCrossProfilePackage(String packageName){ + configurableInteractAcrossProfilePackages.add(packageName); + } + + @Implementation(minSdk = R) + protected void resetInteractAcrossProfilesAppOps( + @NonNull Collection<String> previousCrossProfilePackages, + @NonNull Set<String> newCrossProfilePackages) { + + final List<String> unsetCrossProfilePackages = + previousCrossProfilePackages.stream() + .filter(packageName -> !newCrossProfilePackages.contains(packageName)) + .collect(Collectors.toList()); + + for (String packageName : unsetCrossProfilePackages) { + if (!canConfigureInteractAcrossProfiles(packageName)) { + setInteractAcrossProfilesAppOp(packageName, + AppOpsManager.opToDefaultMode(INTERACT_ACROSS_PROFILES_APPOP)); + } + } + } + + // BEGIN-INTERNAL + @Implementation(minSdk = R) + protected void clearInteractAcrossProfilesAppOps() { + findAllPackageNames().forEach( + packageName -> setInteractAcrossProfilesAppOp( + packageName, AppOpsManager.opToDefaultMode(INTERACT_ACROSS_PROFILES_APPOP))); + } + + private List<String> findAllPackageNames() { + return context.getPackageManager() + .getInstalledApplications(/* flags= */ 0) + .stream() + .map(applicationInfo -> applicationInfo.packageName) + .collect(Collectors.toList()); + } + // END-INTERNAL + + @Implementation + protected boolean canConfigureInteractAcrossProfiles(@NonNull String packageName) { + return configurableInteractAcrossProfilePackages.contains(packageName); + } + + @Implementation + protected boolean canUserAttemptToConfigureInteractAcrossProfiles(@NonNull String packageName) { + PackageInfo packageInfo; + try { + packageInfo = packageManager.getPackageInfo(packageName, /* flags= */ 0); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + if (packageInfo == null || packageInfo.requestedPermissions == null) { + return false; + } + return Arrays.asList(packageInfo.requestedPermissions).contains( + Manifest.permission.INTERACT_ACROSS_PROFILES); + } + + @Resetter + public static void reset() { + configurableInteractAcrossProfilePackages.clear(); + } + // END-INTERNAL + + /** + * Container object to hold parameters passed to {@link #startMainActivity(ComponentName, + * UserHandle)}. + * + * @deprecated Use {@link #peekNextStartedActivity()} and {@link StartedActivity} instead. + */ + @Deprecated + public static class StartedMainActivity { + + private final ComponentName componentName; + private final UserHandle userHandle; + + public StartedMainActivity(ComponentName componentName, UserHandle userHandle) { + this.componentName = checkNotNull(componentName); + this.userHandle = checkNotNull(userHandle); + } + + public ComponentName getComponentName() { + return componentName; + } + + public UserHandle getUserHandle() { + return userHandle; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StartedMainActivity that = (StartedMainActivity) o; + return Objects.equals(componentName, that.componentName) + && Objects.equals(userHandle, that.userHandle); + } + + @Override + public int hashCode() { + return Objects.hash(componentName, userHandle); + } + } + + /** + * Container object to hold parameters passed to {@link #startMainActivity(ComponentName, + * UserHandle)} or {@link #startActivity(ComponentName, UserHandle)}. + */ + public static final class StartedActivity { + + private final ComponentName componentName; + private final UserHandle userHandle; + + public StartedActivity(ComponentName componentName, UserHandle userHandle) { + this.componentName = checkNotNull(componentName); + this.userHandle = checkNotNull(userHandle); + } + + public ComponentName getComponentName() { + return componentName; + } + + public UserHandle getUserHandle() { + return userHandle; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StartedActivity that = (StartedActivity) o; + return Objects.equals(componentName, that.componentName) + && Objects.equals(userHandle, that.userHandle); + } + + @Override + public int hashCode() { + return Objects.hash(componentName, userHandle); + } + } +}
\ No newline at end of file diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java index fdb56ec3e..b5c67555c 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java @@ -9,6 +9,7 @@ import static android.os.Build.VERSION_CODES.N_MR1; import static android.os.Build.VERSION_CODES.O; // BEGIN-INTERNAL import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; // END-INTERNAL import static org.robolectric.shadow.api.Shadow.invokeConstructor; import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; @@ -32,6 +33,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Process; import android.text.TextUtils; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -91,6 +93,7 @@ public class ShadowDevicePolicyManager { private final Map<PackageAndPermission, Integer> appPermissionGrantStateMap = new HashMap<>(); private final Map<ComponentName, byte[]> passwordResetTokens = new HashMap<>(); private final Set<ComponentName> componentsWithActivatedTokens = new HashSet<>(); + private Set<String> defaultCrossProfilePackages = new HashSet<>(); private Collection<String> packagesToFailForSetApplicationHidden = Collections.emptySet(); private Set<String> crossProfileCalendarPackages = Collections.emptySet(); private Context context; @@ -869,5 +872,17 @@ public class ShadowDevicePolicyManager { enforceProfileOwner(admin); crossProfileCalendarPackages = packageNames; } + + @Implementation(minSdk = R) + protected Set<String> getDefaultCrossProfilePackages() { + return new HashSet<>(defaultCrossProfilePackages); + } + + public void addDefaultCrossProfilePackage(String packageName) { + defaultCrossProfilePackages.add(packageName); + } + public void setDefaultCrossProfilePackages(Set<String> defaultCrossProfilePackages) { + this.defaultCrossProfilePackages = new HashSet<>(defaultCrossProfilePackages); + } // END-INTERNAL } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowFingerprintManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowFingerprintManager.java index ea7b6842b..949ff2aa4 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowFingerprintManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowFingerprintManager.java @@ -47,7 +47,7 @@ public class ShadowFingerprintManager { AuthenticationResult result; if (RuntimeEnvironment.getApiLevel() >= N_MR1) { - result = new AuthenticationResult(pendingCryptoObject, null, 0); + result = new AuthenticationResult(pendingCryptoObject, null, 0, true); } else { result = ReflectionHelpers.callConstructor(AuthenticationResult.class, ClassParameter.from(CryptoObject.class, pendingCryptoObject), diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowHardwareRenderer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowHardwareRenderer.java index 458f0fb82..3e17f926f 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowHardwareRenderer.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowHardwareRenderer.java @@ -2,6 +2,7 @@ package org.robolectric.shadows; import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; import android.graphics.HardwareRenderer; import org.robolectric.annotation.Implementation; @@ -16,5 +17,11 @@ public class ShadowHardwareRenderer { protected static long nCreateProxy(boolean translucent, long rootRenderNode) { return ++nextCreateProxy; } + + @Implementation(minSdk = R) + protected static long nCreateProxy( + boolean translucent, boolean isWideGamut, long rootRenderNode) { + return nCreateProxy(translucent, rootRenderNode); + } } // END-INTERNAL
\ No newline at end of file diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java index 87f4d7574..fbe48ac8b 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java @@ -253,34 +253,67 @@ public class ShadowImageDecoder { // native method implementations... - @Implementation + @Implementation(maxSdk = Build.VERSION_CODES.Q) protected static ImageDecoder nCreate(long asset, Source source) throws IOException { return ImageDecoder_nCreateAsset(asset, source); } - @Implementation + @Implementation(maxSdk = Build.VERSION_CODES.Q) protected static ImageDecoder nCreate(ByteBuffer buffer, int position, int limit, Source src) throws IOException { return ImageDecoder_nCreateByteBuffer(buffer, position, limit, src); } - @Implementation + @Implementation(maxSdk = Build.VERSION_CODES.Q) protected static ImageDecoder nCreate(byte[] data, int offset, int length, Source src) throws IOException { return ImageDecoder_nCreateByteArray(data, offset, length, src); } - @Implementation + @Implementation(maxSdk = Build.VERSION_CODES.Q) protected static ImageDecoder nCreate(InputStream is, byte[] storage, Source source) { return ImageDecoder_nCreateInputStream(is, storage, source); } // The fd must be seekable. - @Implementation + @Implementation(maxSdk = Build.VERSION_CODES.Q) protected static ImageDecoder nCreate(FileDescriptor fd, Source src) throws IOException { return ImageDecoder_nCreateFd(fd, src); } + // BEGIN-INTERNAL + @Implementation(minSdk = Build.VERSION_CODES.R) + protected static ImageDecoder nCreate(long asset, boolean preferAnimation, Source source) + throws IOException { + return ImageDecoder_nCreateAsset(asset, source); + } + + @Implementation(minSdk = Build.VERSION_CODES.R) + protected static ImageDecoder nCreate(ByteBuffer buffer, int position, + int limit, boolean preferAnimation, Source src) throws IOException { + return ImageDecoder_nCreateByteBuffer(buffer, position, limit, src); + } + + @Implementation(minSdk = Build.VERSION_CODES.R) + protected static ImageDecoder nCreate(byte[] data, int offset, int length, + boolean preferAnimation, Source src) throws IOException { + return ImageDecoder_nCreateByteArray(data, offset, length, src); + } + + @Implementation(minSdk = Build.VERSION_CODES.R) + protected static ImageDecoder nCreate(InputStream is, byte[] storage, boolean preferAnimation, + Source source) { + return ImageDecoder_nCreateInputStream(is, storage, source); + } + + // The fd must be seekable. + @Implementation(minSdk = Build.VERSION_CODES.R) + protected static ImageDecoder nCreate(FileDescriptor fd, boolean preferAnimation, Source src) + throws IOException { + return ImageDecoder_nCreateFd(fd, src); + } + // END-INTERNAL + @Implementation(maxSdk = Build.VERSION_CODES.P) protected static Bitmap nDecodeBitmap(long nativePtr, ImageDecoder decoder, diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java index 7c5fdaeb3..68587acbc 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInstrumentation.java @@ -172,17 +172,22 @@ public class ShadowInstrumentation { .execStartActivity(who, contextThread, token, target, intent, requestCode, options); } + /** + * Behaves as {@link #execStartActivity(Context, IBinder, IBinder, String, Intent, int, Bundle). + * + * <p>Currently ignores the user. + */ @Implementation(minSdk = JELLY_BEAN_MR1) protected ActivityResult execStartActivity( - Context who, - IBinder contextThread, - IBinder token, - String resultWho, - Intent intent, - int requestCode, - Bundle options, - UserHandle user) { - throw new UnsupportedOperationException("Implement me!!"); + Context who, + IBinder contextThread, + IBinder token, + String resultWho, + Intent intent, + int requestCode, + Bundle options, + UserHandle user) { + return execStartActivity(who, contextThread, token, resultWho, intent, requestCode, options); } @Implementation(minSdk = M) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMotionEvent.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMotionEvent.java index 3189be66b..85a295d7a 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMotionEvent.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMotionEvent.java @@ -338,6 +338,50 @@ public class ShadowMotionEvent { pointerPropertiesObjArray, pointerCoordsObjArray); } + + @Implementation(minSdk = android.os.Build.VERSION_CODES.R) + @HiddenApi + protected static long nativeInitialize( + long nativePtr, + int deviceId, + int source, + int displayId, + int action, + int flags, + int edgeFlags, + int metaState, + int buttonState, + int classification, + float xOffset, + float yOffset, + float xPrecision, + float yPrecision, + long downTimeNanos, + long eventTimeNanos, + int pointerCount, + PointerProperties[] pointerPropertiesObjArray, + PointerCoords[] pointerCoordsObjArray) { + return + nativeInitialize( + nativePtr, + deviceId, + source, + action, + flags, + edgeFlags, + metaState, + buttonState, + xOffset, + yOffset, + xPrecision, + yPrecision, + downTimeNanos, + eventTimeNanos, + pointerCount, + pointerPropertiesObjArray, + pointerCoordsObjArray); + } + // END-INTERNAL @Implementation(maxSdk = KITKAT_WATCH) diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPackageManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPackageManager.java index ff8caad0f..d8a3011c3 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPackageManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPackageManager.java @@ -107,6 +107,7 @@ public class ShadowPackageManager { static final Map<String, String> packageInstallerMap = new HashMap<>(); static final Map<Integer, String[]> packagesForUid = new HashMap<>(); static final Map<String, Integer> uidForPackage = new HashMap<>(); + static final Map<Integer, List<String>> packagesForUserId = new HashMap<>(); static final Map<Integer, String> namesForUid = new HashMap<>(); static final Map<Integer, Integer> verificationResults = new HashMap<>(); static final Map<Integer, Long> verificationTimeoutExtension = new HashMap<>(); @@ -574,6 +575,13 @@ public class ShadowPackageManager { return packagesForUid.get(uid); } + public void setInstalledPackagesForUserId(int userId, List<String> packages) { + packagesForUserId.put(userId, packages); + for (String packageName : packages) { + addPackage(packageName); + } + } + public void setPackageArchiveInfo(String archiveFilePath, PackageInfo packageInfo) { packageArchiveInfo.put(archiveFilePath, packageInfo); } @@ -1115,6 +1123,7 @@ public class ShadowPackageManager { pendingDeleteCallbacks.clear(); hiddenPackages.clear(); sequenceNumberChangedPackagesMap.clear(); + packagesForUserId.clear(); packageSettings.clear(); } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPackageParser.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPackageParser.java index a4f2add69..8c2f6e1fa 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPackageParser.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPackageParser.java @@ -72,15 +72,5 @@ public class ShadowPackageParser { public boolean hasFeature(String feature) { return false; } - - @Override - public String[] getOverlayPaths(String targetPackageName, String targetPath) { - return null; - } - - @Override - public String[] getOverlayApks(String targetPackageName) { - return null; - } } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcel.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcel.java index dcb9719b3..e358fc19b 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcel.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcel.java @@ -7,6 +7,7 @@ import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.O_MR1; import static android.os.Build.VERSION_CODES.P; +import static android.os.Build.VERSION_CODES.R; import static org.robolectric.RuntimeEnvironment.castNativePtr; import android.os.BadParcelableException; @@ -311,6 +312,16 @@ public class ShadowParcel { NATIVE_PTR_TO_PARCEL.get(nativePtr).writeString(val); } + @Implementation(minSdk = R) + protected static void nativeWriteString8(long nativePtr, String val) { + nativeWriteString(nativePtr, val); + } + + @Implementation(minSdk = R) + protected static void nativeWriteString16(long nativePtr, String val) { + nativeWriteString(nativePtr, val); + } + @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) protected static void nativeWriteStrongBinder(int nativePtr, IBinder val) { @@ -399,6 +410,16 @@ public class ShadowParcel { return NATIVE_PTR_TO_PARCEL.get(nativePtr).readString(); } + @Implementation(minSdk = R) + protected static String nativeReadString8(long nativePtr) { + return nativeReadString(nativePtr); + } + + @Implementation(minSdk = R) + protected static String nativeReadString16(long nativePtr) { + return nativeReadString(nativePtr); + } + @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) protected static IBinder nativeReadStrongBinder(int nativePtr) { diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPersistentDataBlockManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPersistentDataBlockManager.java new file mode 100644 index 000000000..a48480df1 --- /dev/null +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPersistentDataBlockManager.java @@ -0,0 +1,42 @@ +/* + * 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 org.robolectric.shadows; + +import android.service.persistentdata.PersistentDataBlockManager; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; + +@Implements(PersistentDataBlockManager.class) +public class ShadowPersistentDataBlockManager { + private static int sDataBlockSize = 0; + + @Resetter + public static void reset() { + sDataBlockSize = 0; + } + + @Implementation + protected int getDataBlockSize() { + return sDataBlockSize; + } + + public static void setDataBlockSize(int dataBlockSize) { + sDataBlockSize = dataBlockSize; + } +} diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowRenderNodeAnimator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowRenderNodeAnimator.java index 61a73f771..ec3a005d2 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowRenderNodeAnimator.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowRenderNodeAnimator.java @@ -7,7 +7,7 @@ import static org.robolectric.shadow.api.Shadow.directlyOn; import android.view.Choreographer; import android.view.Choreographer.FrameCallback; -import android.view.RenderNodeAnimator; +import android.graphics.animation.RenderNodeAnimator; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java index fdbe2b45e..d7dae9b36 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java @@ -9,6 +9,7 @@ import static android.os.Build.VERSION_CODES.N_MR1; import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static android.os.Build.VERSION_CODES.Q; +import static android.os.Build.VERSION_CODES.R; import android.accounts.IAccountManager; import android.app.IAlarmManager; @@ -23,6 +24,7 @@ import android.app.usage.IUsageStatsManager; import android.content.Context; import android.content.IClipboard; import android.content.IRestrictionsManager; +import android.content.pm.ICrossProfileApps; import android.content.pm.IShortcutService; import android.hardware.display.IColorDisplayManager; import android.hardware.fingerprint.IFingerprintService; @@ -35,6 +37,7 @@ import android.media.IMediaRouterService; import android.media.session.ISessionManager; import android.net.IConnectivityManager; import android.net.INetworkScoreService; +import android.net.ITetheringConnector; import android.net.nsd.INsdManager; import android.net.wifi.IWifiManager; import android.net.wifi.p2p.IWifiP2pManager; @@ -44,10 +47,13 @@ import android.os.IBatteryPropertiesRegistrar; import android.os.IBinder; import android.os.IInterface; import android.os.IPowerManager; +import android.os.IThermalService; import android.os.IUserManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.storage.IStorageManager; +import android.service.persistentdata.IPersistentDataBlockService; + import com.android.internal.app.IAppOpsService; import com.android.internal.app.IBatteryStats; import com.android.internal.appwidget.IAppWidgetService; @@ -162,6 +168,10 @@ public class ShadowServiceManager { map.put( Context.MEDIA_SESSION_SERVICE, createDeepBinder(ISessionManager.class, "android.media.session.ISessionManager")); + map.put( + Context.PERSISTENT_DATA_BLOCK_SERVICE, + createBinder(IPersistentDataBlockService.class, + "android.service.persistentdata.IPersistentDataBlockService")); } if (RuntimeEnvironment.getApiLevel() >= M) { map.put( @@ -185,6 +195,9 @@ public class ShadowServiceManager { map.put( Context.SLICE_SERVICE, createBinder(ISliceManager.class, "android.app.slice.SliceManager")); + map.put( + Context.CROSS_PROFILE_APPS_SERVICE, + createBinder(ICrossProfileApps.class, "android.content.pm.ICrossProfileApps")); } // BEGIN-INTERNAL if (RuntimeEnvironment.getApiLevel() >= Q) { @@ -195,6 +208,12 @@ public class ShadowServiceManager { map.put(Context.ROLE_SERVICE, createBinder(IRoleManager.class, "android.app.role.IRoleManager")); } + if (RuntimeEnvironment.getApiLevel() >= R) { + map.put(Context.TETHERING_SERVICE, + createBinder(ITetheringConnector.class, "android.net.ITetheringConnector")); + map.put(Context.THERMAL_SERVICE, + createBinder(IThermalService.class, "android.os.IThermalService")); + } // END-INTERNAL SERVICES = Collections.unmodifiableMap(map); } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSettings.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSettings.java index c7815c2ac..b925f4317 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSettings.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSettings.java @@ -11,12 +11,8 @@ import android.content.Context; import android.os.Build; import android.provider.Settings; import android.text.TextUtils; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.WeakHashMap; +import android.util.ArrayMap; + import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -24,6 +20,16 @@ import org.robolectric.annotation.Resetter; import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers.ClassParameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.stream.Collectors; + @SuppressWarnings({"UnusedDeclaration"}) @Implements(Settings.class) public class ShadowSettings { @@ -244,7 +250,6 @@ public class ShadowSettings { } } - // BEGIN-INTERNAL @Implements(value = Settings.Config.class, minSdk = Build.VERSION_CODES.Q) public static class ShadowConfig { private static final Map<ContentResolver, Map<String, String>> dataMap = new WeakHashMap<>(); @@ -265,6 +270,26 @@ public class ShadowSettings { return get(cr).get(name); } + // BEGIN-INTERNAL + @Implementation(minSdk = Build.VERSION_CODES.R) + protected static Map<String, String> getStrings(ContentResolver cr, String prefix, + List<String> names) { + List<String> concatNames = new ArrayList<>(); + for (String name : names) { + concatNames.add(prefix + "/" + name); + } + + Map<String, String> values = get(cr); + Map<String, String> arrayMap = new ArrayMap<>(); + for (String name : concatNames) { + if (values.containsKey(name)) { + arrayMap.put(name, values.get(name)); + } + } + return arrayMap; + } + // END-INTERNAL + private static Map<String, String> get(ContentResolver cr) { Map<String, String> map = dataMap.get(cr); if (map == null) { @@ -274,7 +299,6 @@ public class ShadowSettings { return map; } } - // END-INTERNAL /** * Sets the value of the {@link Settings.System#AIRPLANE_MODE_ON} setting. diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java index ba6398760..3223c3ca2 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java @@ -6,9 +6,13 @@ import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.N; import static android.os.Build.VERSION_CODES.N_MR1; +import static android.os.Build.VERSION_CODES.R; + import static org.robolectric.shadow.api.Shadow.directlyOn; import android.Manifest.permission; +import android.annotation.NonNull; +import android.annotation.UserIdInt; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.UserInfo; @@ -17,17 +21,23 @@ import android.os.IUserManager; import android.os.Process; import android.os.UserHandle; import android.os.UserManager; + import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableList; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; + import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.RealObject; import org.robolectric.annotation.Resetter; +import org.robolectric.util.ReflectionHelpers.ClassParameter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; /** * Robolectric implementation of {@link android.os.UserManager}. @@ -44,6 +54,7 @@ public class ShadowUserManager { public static final int FLAG_ADMIN = UserInfo.FLAG_ADMIN; public static final int FLAG_GUEST = UserInfo.FLAG_GUEST; public static final int FLAG_RESTRICTED = UserInfo.FLAG_RESTRICTED; + public static final int FLAG_MANAGED_PROFILE = UserInfo.FLAG_MANAGED_PROFILE; private static Map<Integer, Integer> userPidMap = new HashMap<>(); @@ -58,6 +69,8 @@ public class ShadowUserManager { private long nextUserSerial = 0; private Map<Integer, UserState> userState = new HashMap<>(); private Map<Integer, UserInfo> userInfoMap = new HashMap<>(); + private Map<Integer, List<UserInfo>> profiles = new HashMap<>(); + private Map<Integer, Integer> profileToParent = new HashMap<>(); private Context context; private boolean enforcePermissions; @@ -106,6 +119,47 @@ public class ShadowUserManager { return ImmutableList.copyOf(userProfiles.keySet()); } + /** + * If any profiles have been added using {@link #addProfile}, return those profiles. + * + * Otherwise follow real android behaviour. + */ + @Implementation(minSdk = LOLLIPOP) + protected List<UserInfo> getProfiles(int userHandle) { + if (profiles.containsKey(userHandle)) { + return ImmutableList.copyOf(profiles.get(userHandle)); + } + + if (profileToParent.containsKey(userHandle) + && profiles.containsKey(profileToParent.get(userHandle))) { + return ImmutableList.copyOf(profiles.get(profileToParent.get(userHandle))); + } + + return directlyOn( + realObject, UserManager.class, "getProfiles", ClassParameter.from(int.class, userHandle)); + } + + /** Add a profile to be returned by {@link #getProfiles(int)}.**/ + public void addProfile( + int userHandle, int profileUserHandle, String profileName, int profileFlags) { + UserInfo userInfo = new UserInfo(profileUserHandle, profileName, profileFlags); + profiles.putIfAbsent(userHandle, new ArrayList<>()); + profiles.get(userHandle).add(userInfo); + userInfoMap.put(profileUserHandle, userInfo); + profileToParent.put(profileUserHandle, userHandle); + } + + /** + * If this profile has been added using {@link #addProfile}, return its parent. + */ + @Implementation(minSdk = LOLLIPOP) + protected UserInfo getProfileParent(int userHandle) { + if (!profileToParent.containsKey(userHandle)) { + return null; + } + return userInfoMap.get(profileToParent.get(userHandle)); + } + @Implementation(minSdk = N) protected boolean isUserUnlocked() { return userUnlocked; @@ -131,12 +185,52 @@ public class ShadowUserManager { protected boolean isManagedProfile() { if (enforcePermissions && !hasManageUsersPermission()) { throw new SecurityException( - "You need MANAGE_USERS permission to: check if specified user a " + - "managed profile outside your profile group"); + "You need MANAGE_USERS permission to: check if specified user a " + + "managed profile outside your profile group"); } return managedProfile; } + /** + * If permissions are enforced (see {@link #enforcePermissionChecks(boolean)}) and the application + * doesn't have the {@link android.Manifest.permission#MANAGE_USERS} permission, throws a {@link + * SecurityManager} exception. + * + * @return true if the profile added has FLAG_MANAGED_PROFILE + * @see #enforcePermissionChecks(boolean) + * @see #addProfile(int, int, String, int) + * @see #addUser(int, String, int) + */ + @Implementation(minSdk = N) + protected boolean isManagedProfile(int userHandle) { + if (enforcePermissions && !hasManageUsersPermission()) { + throw new SecurityException( + "You need MANAGE_USERS permission to: check if specified user a " + + "managed profile outside your profile group"); + } + UserInfo info = getUserInfo(userHandle); + return info != null && ((info.flags & FLAG_MANAGED_PROFILE) == FLAG_MANAGED_PROFILE); + } + + // BEGIN-INTERNAL + @Implementation(minSdk = R) + protected boolean isProfile() { + return isManagedProfile(); + } + + /** + * Compared to real Android, userId is not used, instead + * managedProfile determines if user has badge. + * + * @param userId ignored, uses managedProfile field + * @return true if managedProfile field is true + */ + @Implementation(minSdk = R) + protected boolean hasBadge(int userId) { + return isProfile(); + } + // END-INTERNAL + public void enforcePermissionChecks(boolean enforcePermissions) { this.enforcePermissions = enforcePermissions; } @@ -154,6 +248,13 @@ public class ShadowUserManager { return bundle != null && bundle.getBoolean(restrictionKey); } + // BEGIN-INTERNAL + @Implementation(minSdk = R) + protected boolean hasUserRestrictionForUser(String restrictionKey, UserHandle userHandle) { + return hasUserRestriction(restrictionKey, userHandle); + } + // END-INTERNAL + public void setUserRestriction(UserHandle userHandle, String restrictionKey, boolean value) { Bundle bundle = getUserRestrictionsForUser(userHandle); bundle.putBoolean(restrictionKey, value); @@ -208,6 +309,16 @@ public class ShadowUserManager { return userProfiles.inverse().get(serialNumber); } + /** + * @see #addProfile(int, int, String, int) + * @see #addUser(int, String, int) + */ + @Implementation + protected int getUserSerialNumber(@UserIdInt int userHandle) { + Long result = userProfiles.get(UserHandle.of(userHandle)); + return result != null ? result.intValue() : -1; + } + private boolean hasManageUsersPermission() { return context.getPackageManager().checkPermission(permission.MANAGE_USERS, context.getPackageName()) == PackageManager.PERMISSION_GRANTED; } @@ -357,8 +468,8 @@ public class ShadowUserManager { UserState state = userState.get(handle.getIdentifier()); if (state == UserState.STATE_RUNNING_LOCKED - || state == UserState.STATE_RUNNING_UNLOCKED - || state == UserState.STATE_RUNNING_UNLOCKING) { + || state == UserState.STATE_RUNNING_UNLOCKED + || state == UserState.STATE_RUNNING_UNLOCKING) { return true; } else { return false; @@ -374,9 +485,9 @@ public class ShadowUserManager { UserState state = userState.get(handle.getIdentifier()); if (state == UserState.STATE_RUNNING_LOCKED - || state == UserState.STATE_RUNNING_UNLOCKED - || state == UserState.STATE_RUNNING_UNLOCKING - || state == UserState.STATE_STOPPING) { + || state == UserState.STATE_RUNNING_UNLOCKED + || state == UserState.STATE_RUNNING_UNLOCKING + || state == UserState.STATE_STOPPING) { return true; } else { return false; @@ -442,6 +553,29 @@ public class ShadowUserManager { return true; } + // BEGIN-INTERNAL + @Implementation(minSdk = R) + protected UserInfo createProfileForUserEvenWhenDisallowed(String name, + @NonNull String userType, @UserInfo.UserInfoFlag int flags, @UserIdInt int userId, + String[] disallowedPackages) throws UserManager.UserOperationException { + List<UserInfo> userIdProfiles = profiles.computeIfAbsent(userId, ignored -> new ArrayList<>()); + int profileUserId = userIdProfiles.isEmpty() ? 10 : findMaxProfileId(userIdProfiles) + 1; + UserInfo profileUserInfo = new UserInfo(profileUserId, name, flags); + userIdProfiles.add(profileUserInfo); + profileToParent.put(profileUserId, userId); + addUserProfile(UserHandle.of(profileUserId)); + return profileUserInfo; + } + + /** Assumes the given list of profile infos is non-empty. */ + private int findMaxProfileId(List<UserInfo> userIdProfiles) { + return Collections.max( + userIdProfiles.stream() + .map(userInfo -> userInfo.id) + .collect(Collectors.toList())); + } + // END-INTERNAL + /** * Switches the current user to {@code userHandle}. * @@ -464,15 +598,16 @@ public class ShadowUserManager { */ public void addUser(int id, String name, int flags) { UserHandle userHandle = - id == UserHandle.USER_SYSTEM ? Process.myUserHandle() : new UserHandle(id); + id == UserHandle.USER_SYSTEM ? Process.myUserHandle() : new UserHandle(id); addUserProfile(userHandle); setSerialNumberForUser(userHandle, (long) id); + profiles.putIfAbsent(id, new ArrayList<>()); userInfoMap.put(id, new UserInfo(id, name, flags)); userPidMap.put( - id, - id == UserHandle.USER_SYSTEM - ? Process.myUid() - : id * UserHandle.PER_USER_RANGE + ShadowProcess.getRandomApplicationUid()); + id, + id == UserHandle.USER_SYSTEM + ? Process.myUid() + : id * UserHandle.PER_USER_RANGE + ShadowProcess.getRandomApplicationUid()); } @Resetter @@ -484,4 +619,4 @@ public class ShadowUserManager { userPidMap.put(UserHandle.USER_SYSTEM, Process.myUid()); } } -} +}
\ No newline at end of file diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java index 2730cef99..964cdda19 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java @@ -116,8 +116,7 @@ public class ShadowViewRootImpl { ClassParameter.from(boolean.class, false), ClassParameter.from(boolean.class, false), ClassParameter.from(int.class, 0)); - - } else if (apiLevel >= Build.VERSION_CODES.P) { + } else if (apiLevel <= Build.VERSION_CODES.P) { ReflectionHelpers.callInstanceMethod(ViewRootImpl.class, component, "dispatchResized", ClassParameter.from(Rect.class, frame), ClassParameter.from(Rect.class, zeroSizedRect), @@ -133,7 +132,22 @@ public class ShadowViewRootImpl { ClassParameter.from(int.class, 0), ClassParameter.from(android.view.DisplayCutout.ParcelableWrapper.class, new android.view.DisplayCutout.ParcelableWrapper())); - + } else if (apiLevel >= Build.VERSION_CODES.R) { + // BEGIN-INTERNAL + ReflectionHelpers.callInstanceMethod(ViewRootImpl.class, component, "dispatchResized", + ClassParameter.from(Rect.class, frame), + ClassParameter.from(Rect.class, zeroSizedRect), + ClassParameter.from(Rect.class, zeroSizedRect), + ClassParameter.from(Rect.class, zeroSizedRect), + ClassParameter.from(boolean.class, true), + ClassParameter.from(MergedConfiguration.class, new MergedConfiguration()), + ClassParameter.from(Rect.class, frame), + ClassParameter.from(boolean.class, false), + ClassParameter.from(boolean.class, false), + ClassParameter.from(int.class, 0), + ClassParameter.from(android.view.DisplayCutout.ParcelableWrapper.class, + new android.view.DisplayCutout.ParcelableWrapper())); + // END-INTERNAL } else { throw new RuntimeException("Could not find AndroidRuntimeAdapter for API level: " + apiLevel); } diff --git a/soong/robolectric.go b/soong/robolectric.go index 836cd158f..0ef5bd29e 100644 --- a/soong/robolectric.go +++ b/soong/robolectric.go @@ -77,6 +77,9 @@ func (b *buildProps) GenerateAndroidBuildActions(ctx android.ModuleContext) { "ro.product.cpu.abilist=armeabi-v7a", "ro.product.cpu.abilist32=armeabi-v7a,armeabi", "ro.product.cpu.abilist64=armeabi-v7a,armeabi", + "", + "# temp fix for robolectric freezing issue b/150011638", + "persist.debug.new_insets=0", } b.output = android.PathForModuleGen(ctx, "build.prop") |
