diff --git a/Ravenwood.bp b/Ravenwood.bp index 4e360759c1376c02b7c8ef5bb46604d41ae98a09..c73e04896173feaf3d158f93a61171cefa88cf9f 100644 --- a/Ravenwood.bp +++ b/Ravenwood.bp @@ -144,6 +144,16 @@ java_library { jarjar_rules: ":ravenwood-services-jarjar-rules", } +java_library { + name: "services.fakes.ravenwood-jarjar", + installable: false, + srcs: [":services.fakes-sources"], + libs: [ + "services.core.ravenwood", + ], + jarjar_rules: ":ravenwood-services-jarjar-rules", +} + java_library { name: "mockito-ravenwood-prebuilt", installable: false, @@ -189,6 +199,7 @@ android_ravenwood_libgroup { "ravenwood-helper-runtime", "hoststubgen-helper-runtime.ravenwood", "services.core.ravenwood-jarjar", + "services.fakes.ravenwood-jarjar", // Provide runtime versions of utils linked in below "junit", diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java index 728c350bfb516bba93d4abf14186df3db6a40675..b42133939f28dd5cf35dc00befc28a9bc631f51b 100644 --- a/core/java/android/content/ClipData.java +++ b/core/java/android/content/ClipData.java @@ -169,6 +169,8 @@ import java.util.List; */ @android.ravenwood.annotation.RavenwoodKeepWholeClass public class ClipData implements Parcelable { + private static final String TAG = "ClipData"; + static final String[] MIMETYPES_TEXT_PLAIN = new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN }; static final String[] MIMETYPES_TEXT_HTML = new String[] { @@ -476,7 +478,6 @@ public class ClipData implements Parcelable { * @return Returns the item's textual representation. */ //BEGIN_INCLUDE(coerceToText) - @android.ravenwood.annotation.RavenwoodThrow public CharSequence coerceToText(Context context) { // If this Item has an explicit textual value, simply return that. CharSequence text = getText(); @@ -484,13 +485,20 @@ public class ClipData implements Parcelable { return text; } + // Gracefully handle cases where resolver isn't available + ContentResolver resolver = null; + try { + resolver = context.getContentResolver(); + } catch (Exception e) { + Log.w(TAG, "Failed to obtain ContentResolver: " + e); + } + // If this Item has a URI value, try using that. Uri uri = getUri(); - if (uri != null) { + if (uri != null && resolver != null) { // First see if the URI can be opened as a plain text stream // (of any sub-type). If so, this is the best textual // representation for it. - final ContentResolver resolver = context.getContentResolver(); AssetFileDescriptor descr = null; FileInputStream stream = null; InputStreamReader reader = null; @@ -499,7 +507,7 @@ public class ClipData implements Parcelable { // Ask for a stream of the desired type. descr = resolver.openTypedAssetFileDescriptor(uri, "text/*", null); } catch (SecurityException e) { - Log.w("ClipData", "Failure opening stream", e); + Log.w(TAG, "Failure opening stream", e); } catch (FileNotFoundException|RuntimeException e) { // Unable to open content URI as text... not really an // error, just something to ignore. @@ -519,7 +527,7 @@ public class ClipData implements Parcelable { return builder.toString(); } catch (IOException e) { // Something bad has happened. - Log.w("ClipData", "Failure loading text", e); + Log.w(TAG, "Failure loading text", e); return e.toString(); } } @@ -528,7 +536,8 @@ public class ClipData implements Parcelable { IoUtils.closeQuietly(stream); IoUtils.closeQuietly(reader); } - + } + if (uri != null) { // If we couldn't open the URI as a stream, use the URI itself as a textual // representation (but not for "content", "android.resource" or "file" schemes). final String scheme = uri.getScheme(); @@ -704,7 +713,7 @@ public class ClipData implements Parcelable { } } catch (SecurityException e) { - Log.w("ClipData", "Failure opening stream", e); + Log.w(TAG, "Failure opening stream", e); } catch (FileNotFoundException e) { // Unable to open content URI as text... not really an @@ -712,7 +721,7 @@ public class ClipData implements Parcelable { } catch (IOException e) { // Something bad has happened. - Log.w("ClipData", "Failure loading text", e); + Log.w(TAG, "Failure loading text", e); return Html.escapeHtml(e.toString()); } finally { @@ -1123,7 +1132,7 @@ public class ClipData implements Parcelable { * * @hide */ - @android.ravenwood.annotation.RavenwoodThrow + @android.ravenwood.annotation.RavenwoodKeep public void prepareToLeaveProcess(boolean leavingPackage) { // Assume that callers are going to be granting permissions prepareToLeaveProcess(leavingPackage, Intent.FLAG_GRANT_READ_URI_PERMISSION); @@ -1134,7 +1143,7 @@ public class ClipData implements Parcelable { * * @hide */ - @android.ravenwood.annotation.RavenwoodThrow + @android.ravenwood.annotation.RavenwoodReplace public void prepareToLeaveProcess(boolean leavingPackage, int intentFlags) { final int size = mItems.size(); for (int i = 0; i < size; i++) { @@ -1154,6 +1163,11 @@ public class ClipData implements Parcelable { } } + /** @hide */ + public void prepareToLeaveProcess$ravenwood(boolean leavingPackage, int intentFlags) { + // No process boundaries on Ravenwood; ignored + } + /** {@hide} */ @android.ravenwood.annotation.RavenwoodThrow public void prepareToEnterProcess(AttributionSource source) { diff --git a/core/java/android/content/ClipboardManager.java b/core/java/android/content/ClipboardManager.java index 107f1078b11e3f4f02955fbb2f559d8e1ece5998..2fabcbae9bbbb3497a393a0ac3dff7c7e54a9ba2 100644 --- a/core/java/android/content/ClipboardManager.java +++ b/core/java/android/content/ClipboardManager.java @@ -50,6 +50,7 @@ import java.util.Objects; * </div> */ @SystemService(Context.CLIPBOARD_SERVICE) +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class ClipboardManager extends android.text.ClipboardManager { /** @@ -143,6 +144,7 @@ public class ClipboardManager extends android.text.ClipboardManager { */ @SystemApi @RequiresPermission(Manifest.permission.MANAGE_CLIPBOARD_ACCESS_NOTIFICATION) + @android.ravenwood.annotation.RavenwoodThrow public boolean areClipboardAccessNotificationsEnabled() { try { return mService.areClipboardAccessNotificationsEnabledForUser(mContext.getUserId()); @@ -159,6 +161,7 @@ public class ClipboardManager extends android.text.ClipboardManager { */ @SystemApi @RequiresPermission(Manifest.permission.MANAGE_CLIPBOARD_ACCESS_NOTIFICATION) + @android.ravenwood.annotation.RavenwoodThrow public void setClipboardAccessNotificationsEnabled(boolean enable) { try { mService.setClipboardAccessNotificationsEnabledForUser(enable, mContext.getUserId()); diff --git a/core/java/android/os/HandlerThread.java b/core/java/android/os/HandlerThread.java index 36730cb07344f801b559c02d3f780cf032a8c3b7..f852d3cd69b285806b8281157c5c05a35a5d1ce1 100644 --- a/core/java/android/os/HandlerThread.java +++ b/core/java/android/os/HandlerThread.java @@ -19,6 +19,8 @@ package android.os; import android.annotation.NonNull; import android.annotation.Nullable; +import java.util.concurrent.Executor; + /** * A {@link Thread} that has a {@link Looper}. * The {@link Looper} can then be used to create {@link Handler}s. @@ -30,7 +32,8 @@ public class HandlerThread extends Thread { int mPriority; int mTid = -1; Looper mLooper; - private @Nullable Handler mHandler; + private volatile @Nullable Handler mHandler; + private volatile @Nullable Executor mExecutor; public HandlerThread(String name) { super(name); @@ -130,6 +133,18 @@ public class HandlerThread extends Thread { return mHandler; } + /** + * @return a shared {@link Executor} associated with this thread + * @hide + */ + @NonNull + public Executor getThreadExecutor() { + if (mExecutor == null) { + mExecutor = new HandlerExecutor(getThreadHandler()); + } + return mExecutor; + } + /** * Quits the handler thread's looper. * <p> diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt index 6b6736476210882939cd797d2682047cab023175..371c3acab1440543834f03ba9e7b10a23bc40ef3 100644 --- a/ravenwood/framework-minus-apex-ravenwood-policies.txt +++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt @@ -55,3 +55,5 @@ class android.content.Context stub method getSystemService (Ljava/lang/Class;)Ljava/lang/Object; stub class android.content.pm.PackageManager stub method <init> ()V stub +class android.text.ClipboardManager stub + method <init> ()V stub diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java index 3668b03e58d3e72cad01ec5c588729f332ea11b0..c17d0903f8560c03eee8c60d3d2a15768ad66318 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java @@ -16,18 +16,28 @@ package android.platform.test.ravenwood; +import android.content.ClipboardManager; import android.content.Context; import android.hardware.ISerialManager; import android.hardware.SerialManager; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; import android.os.PermissionEnforcer; import android.os.ServiceManager; +import android.os.UserHandle; import android.test.mock.MockContext; import android.util.ArrayMap; import android.util.Singleton; +import java.util.Objects; +import java.util.concurrent.Executor; import java.util.function.Supplier; public class RavenwoodContext extends MockContext { + private final String mPackageName; + private final HandlerThread mMainThread; + private final RavenwoodPermissionEnforcer mEnforcer = new RavenwoodPermissionEnforcer(); private final ArrayMap<Class<?>, String> mClassToName = new ArrayMap<>(); @@ -39,7 +49,13 @@ public class RavenwoodContext extends MockContext { mNameToFactory.put(serviceName, serviceSupplier); } - public RavenwoodContext() { + public RavenwoodContext(String packageName, HandlerThread mainThread) { + mPackageName = packageName; + mMainThread = mainThread; + + registerService(ClipboardManager.class, + Context.CLIPBOARD_SERVICE, asSingleton(() -> + new ClipboardManager(this, getMainThreadHandler()))); registerService(PermissionEnforcer.class, Context.PERMISSION_ENFORCER_SERVICE, () -> mEnforcer); registerService(SerialManager.class, @@ -73,18 +89,79 @@ public class RavenwoodContext extends MockContext { } } + @Override + public Looper getMainLooper() { + Objects.requireNonNull(mMainThread, + "Test must request setProvideMainThread() via RavenwoodRule"); + return mMainThread.getLooper(); + } + + @Override + public Handler getMainThreadHandler() { + Objects.requireNonNull(mMainThread, + "Test must request setProvideMainThread() via RavenwoodRule"); + return mMainThread.getThreadHandler(); + } + + @Override + public Executor getMainExecutor() { + Objects.requireNonNull(mMainThread, + "Test must request setProvideMainThread() via RavenwoodRule"); + return mMainThread.getThreadExecutor(); + } + + @Override + public String getPackageName() { + return Objects.requireNonNull(mPackageName, + "Test must request setPackageName() via RavenwoodRule"); + } + + @Override + public String getOpPackageName() { + return Objects.requireNonNull(mPackageName, + "Test must request setPackageName() via RavenwoodRule"); + } + + @Override + public String getAttributionTag() { + return null; + } + + @Override + public UserHandle getUser() { + return android.os.UserHandle.of(android.os.UserHandle.myUserId()); + } + + @Override + public int getUserId() { + return android.os.UserHandle.myUserId(); + } + + @Override + public int getDeviceId() { + return Context.DEVICE_ID_DEFAULT; + } + /** * Wrap the given {@link Supplier} to become a memoized singleton. */ - private static <T> Supplier<T> asSingleton(Supplier<T> supplier) { + private static <T> Supplier<T> asSingleton(ThrowingSupplier<T> supplier) { final Singleton<T> singleton = new Singleton<>() { @Override protected T create() { - return supplier.get(); + try { + return supplier.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } } }; return () -> { return singleton.get(); }; } + + public interface ThrowingSupplier<T> { + T get() throws Exception; + } } diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index 231cce95f353d5e84953e4aa019cdcf89b2704cd..56a3c64a57502ae23fbe04066f1f50166a6f0a2f 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -110,13 +110,16 @@ public class RavenwoodRuleImpl { ActivityManager.init$ravenwood(rule.mCurrentUser); + final HandlerThread main; if (rule.mProvideMainThread) { - final HandlerThread main = new HandlerThread(MAIN_THREAD_NAME); + main = new HandlerThread(MAIN_THREAD_NAME); main.start(); Looper.setMainLooperForTest(main.getLooper()); + } else { + main = null; } - rule.mContext = new RavenwoodContext(); + rule.mContext = new RavenwoodContext(rule.mPackageName, main); rule.mInstrumentation = new Instrumentation(); rule.mInstrumentation.basicInit(rule.mContext); InstrumentationRegistry.registerInstance(rule.mInstrumentation, Bundle.EMPTY); diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java index bb280f47ccd929972dda02cc90ded35eeda1b920..3de96c0990ea3cc23cb1cb477c66d2f690b1014b 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java @@ -16,6 +16,7 @@ package android.platform.test.ravenwood; +import android.content.ClipboardManager; import android.hardware.SerialManager; import android.os.SystemClock; import android.util.ArrayMap; @@ -40,7 +41,10 @@ public class RavenwoodSystemServer { // authors to exhaustively declare all transitive services static { - sKnownServices.put(SerialManager.class, "com.android.server.SerialService$Lifecycle"); + sKnownServices.put(ClipboardManager.class, + "com.android.server.FakeClipboardService$Lifecycle"); + sKnownServices.put(SerialManager.class, + "com.android.server.SerialService$Lifecycle"); } private static TimingsTraceAndSlog sTimings; diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index a8c24fcbd7e0be053c840582c61af17bca9ccf90..a520d4ccafa1011961b1ae89145b06f7ce51b03f 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -121,6 +121,8 @@ public class RavenwoodRule implements TestRule { int mUid = NOBODY_UID; int mPid = sNextPid.getAndIncrement(); + String mPackageName; + boolean mProvideMainThread = false; final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties(); @@ -157,6 +159,15 @@ public class RavenwoodRule implements TestRule { return this; } + /** + * Configure the identity of this process to be the given package name for the duration + * of the test. Has no effect on non-Ravenwood environments. + */ + public Builder setPackageName(/* @NonNull */ String packageName) { + mRule.mPackageName = Objects.requireNonNull(packageName); + return this; + } + /** * Configure a "main" thread to be available for the duration of the test, as defined * by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments. diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt index eb3c55cb4ff69f4e322dfea169901eae18b825fb..9b4d378cc7b79aab8cb09579b539a9b498a48e91 100644 --- a/ravenwood/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/ravenwood-annotation-allowed-classes.txt @@ -186,6 +186,7 @@ android.os.WorkSource android.content.ClipData android.content.ClipData$Item android.content.ClipDescription +android.content.ClipboardManager android.content.ComponentName android.content.ContentUris android.content.ContentValues diff --git a/services/fakes/Android.bp b/services/fakes/Android.bp new file mode 100644 index 0000000000000000000000000000000000000000..148054b31e89ed5f3d381fef55a838111019a659 --- /dev/null +++ b/services/fakes/Android.bp @@ -0,0 +1,20 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +// NOTE: These "fake" services are intended for use under the Ravenwood +// deviceless test environment, and should *not* be included in the build +// artifacts for physical devices, as they already supply "real" services +filegroup { + name: "services.fakes-sources", + srcs: [ + "java/**/*.java", + ], + path: "java", + visibility: ["//frameworks/base"], +} diff --git a/services/fakes/java/com/android/server/FakeClipboardService.java b/services/fakes/java/com/android/server/FakeClipboardService.java new file mode 100644 index 0000000000000000000000000000000000000000..01016219e73de26adb416b4da4ad83e49a1ab35d --- /dev/null +++ b/services/fakes/java/com/android/server/FakeClipboardService.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.Context; +import android.content.IClipboard; +import android.content.IOnPrimaryClipChangedListener; +import android.os.PermissionEnforcer; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.UserHandle; + +import com.android.internal.util.Preconditions; + +/** + * Fake implementation of {@code ClipboardManager} since the real implementation is tightly + * coupled with many other internal services. + */ +public class FakeClipboardService extends IClipboard.Stub { + private final RemoteCallbackList<IOnPrimaryClipChangedListener> mListeners = + new RemoteCallbackList<>(); + + private ClipData mPrimaryClip; + private String mPrimaryClipSource; + + public FakeClipboardService(Context context) { + super(PermissionEnforcer.fromContext(context)); + } + + public static class Lifecycle extends SystemService { + private FakeClipboardService mService; + + public Lifecycle(Context context) { + super(context); + } + + @Override + public void onStart() { + mService = new FakeClipboardService(getContext()); + publishBinderService(Context.CLIPBOARD_SERVICE, mService); + } + } + + private static void checkArguments(int userId, int deviceId) { + Preconditions.checkArgument(userId == UserHandle.USER_SYSTEM, + "Fake only supports USER_SYSTEM user"); + Preconditions.checkArgument(deviceId == Context.DEVICE_ID_DEFAULT, + "Fake only supports DEVICE_ID_DEFAULT device"); + } + + private void dispatchPrimaryClipChanged() { + mListeners.broadcast((listener) -> { + try { + listener.dispatchPrimaryClipChanged(); + } catch (RemoteException ignored) { + } + }); + } + + @Override + public void setPrimaryClip(ClipData clip, String callingPackage, String attributionTag, + int userId, int deviceId) { + checkArguments(userId, deviceId); + mPrimaryClip = clip; + mPrimaryClipSource = callingPackage; + dispatchPrimaryClipChanged(); + } + + @Override + @android.annotation.EnforcePermission(android.Manifest.permission.SET_CLIP_SOURCE) + public void setPrimaryClipAsPackage(ClipData clip, String callingPackage, String attributionTag, + int userId, int deviceId, String sourcePackage) { + setPrimaryClipAsPackage_enforcePermission(); + checkArguments(userId, deviceId); + mPrimaryClip = clip; + mPrimaryClipSource = sourcePackage; + dispatchPrimaryClipChanged(); + } + + @Override + public void clearPrimaryClip(String callingPackage, String attributionTag, int userId, + int deviceId) { + checkArguments(userId, deviceId); + mPrimaryClip = null; + mPrimaryClipSource = null; + dispatchPrimaryClipChanged(); + } + + @Override + public ClipData getPrimaryClip(String pkg, String attributionTag, int userId, int deviceId) { + checkArguments(userId, deviceId); + return mPrimaryClip; + } + + @Override + public ClipDescription getPrimaryClipDescription(String callingPackage, String attributionTag, + int userId, int deviceId) { + checkArguments(userId, deviceId); + return (mPrimaryClip != null) ? mPrimaryClip.getDescription() : null; + } + + @Override + public boolean hasPrimaryClip(String callingPackage, String attributionTag, int userId, + int deviceId) { + checkArguments(userId, deviceId); + return mPrimaryClip != null; + } + + @Override + public void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener, + String callingPackage, String attributionTag, int userId, int deviceId) { + checkArguments(userId, deviceId); + mListeners.register(listener); + } + + @Override + public void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener listener, + String callingPackage, String attributionTag, int userId, int deviceId) { + checkArguments(userId, deviceId); + mListeners.unregister(listener); + } + + @Override + public boolean hasClipboardText(String callingPackage, String attributionTag, int userId, + int deviceId) { + checkArguments(userId, deviceId); + return (mPrimaryClip != null) && (mPrimaryClip.getItemCount() > 0) + && (mPrimaryClip.getItemAt(0).getText() != null); + } + + @Override + @android.annotation.EnforcePermission(android.Manifest.permission.SET_CLIP_SOURCE) + public String getPrimaryClipSource(String callingPackage, String attributionTag, int userId, + int deviceId) { + getPrimaryClipSource_enforcePermission(); + checkArguments(userId, deviceId); + return mPrimaryClipSource; + } + + @Override + public boolean areClipboardAccessNotificationsEnabledForUser(int userId) { + throw new UnsupportedOperationException(); + } + + @Override + public void setClipboardAccessNotificationsEnabledForUser(boolean enable, int userId) { + throw new UnsupportedOperationException(); + } +}