diff options
89 files changed, 1860 insertions, 367 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index adb4eaca2d11..4aa820a605b2 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -1604,6 +1604,7 @@ package android { field public static final int switchTextOff = 16843628; // 0x101036c field public static final int switchTextOn = 16843627; // 0x101036b field public static final int syncable = 16842777; // 0x1010019 + field @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") public static final int systemUserOnly; field public static final int tabStripEnabled = 16843453; // 0x10102bd field public static final int tabStripLeft = 16843451; // 0x10102bb field public static final int tabStripRight = 16843452; // 0x10102bc @@ -52463,9 +52464,9 @@ package android.view { field protected static final int[] PRESSED_STATE_SET; field protected static final int[] PRESSED_WINDOW_FOCUSED_STATE_SET; field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_DEFAULT = 0.0f; - field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_HIGH = -120.0f; - field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_LOW = -30.0f; - field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_NORMAL = -60.0f; + field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_HIGH = -4.0f; + field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_LOW = -2.0f; + field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_NORMAL = -3.0f; field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE = -1.0f; field public static final android.util.Property<android.view.View,java.lang.Float> ROTATION; field public static final android.util.Property<android.view.View,java.lang.Float> ROTATION_X; @@ -57119,7 +57120,7 @@ package android.webkit { method public abstract boolean getBuiltInZoomControls(); method public abstract int getCacheMode(); method public abstract String getCursiveFontFamily(); - method @Deprecated public abstract boolean getDatabaseEnabled(); + method public abstract boolean getDatabaseEnabled(); method @Deprecated public abstract String getDatabasePath(); method public abstract int getDefaultFixedFontSize(); method public abstract int getDefaultFontSize(); @@ -57165,7 +57166,7 @@ package android.webkit { method public abstract void setBuiltInZoomControls(boolean); method public abstract void setCacheMode(int); method public abstract void setCursiveFontFamily(String); - method @Deprecated public abstract void setDatabaseEnabled(boolean); + method public abstract void setDatabaseEnabled(boolean); method @Deprecated public abstract void setDatabasePath(String); method public abstract void setDefaultFixedFontSize(int); method public abstract void setDefaultFontSize(int); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 949e2ba07a18..e288b42f7ec7 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -6834,23 +6834,24 @@ public final class ActivityThread extends ClientTransactionHandler PackageManager.GET_SHARED_LIBRARY_FILES, UserHandle.myUserId()); - if (mActivities.size() > 0) { - for (ActivityClientRecord ar : mActivities.values()) { - if (ar.activityInfo.applicationInfo.packageName - .equals(packageName)) { - ar.activityInfo.applicationInfo = aInfo; - ar.packageInfo = pkgInfo; + if (aInfo != null) { + if (mActivities.size() > 0) { + for (ActivityClientRecord ar : mActivities.values()) { + if (ar.activityInfo.applicationInfo.packageName + .equals(packageName)) { + ar.activityInfo.applicationInfo = aInfo; + ar.packageInfo = pkgInfo; + } } } - } - final String[] oldResDirs = { pkgInfo.getResDir() }; + final String[] oldResDirs = {pkgInfo.getResDir()}; - final ArrayList<String> oldPaths = new ArrayList<>(); - LoadedApk.makePaths(this, pkgInfo.getApplicationInfo(), oldPaths); - pkgInfo.updateApplicationInfo(aInfo, oldPaths); + final ArrayList<String> oldPaths = new ArrayList<>(); + LoadedApk.makePaths( + this, pkgInfo.getApplicationInfo(), oldPaths); + pkgInfo.updateApplicationInfo(aInfo, oldPaths); - synchronized (mResourcesManager) { // Update affected Resources objects to use new ResourcesImpl mResourcesManager.appendPendingAppInfoUpdate(oldResDirs, aInfo); diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index c7a75ed5ea9c..e9b94c9f5791 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -41,6 +41,7 @@ import android.content.res.Configuration; import android.database.Cursor; import android.database.MatrixCursor; import android.database.SQLException; +import android.multiuser.Flags; import android.net.Uri; import android.os.AsyncTask; import android.os.Binder; @@ -146,6 +147,7 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall private boolean mExported; private boolean mNoPerms; private boolean mSingleUser; + private boolean mSystemUserOnly; private SparseBooleanArray mUsersRedirectedToOwnerForMedia = new SparseBooleanArray(); private ThreadLocal<AttributionSource> mCallingAttributionSource; @@ -377,7 +379,9 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall != PermissionChecker.PERMISSION_GRANTED && getContext().checkUriPermission(userUri, Binder.getCallingPid(), callingUid, Intent.FLAG_GRANT_READ_URI_PERMISSION) - != PackageManager.PERMISSION_GRANTED) { + != PackageManager.PERMISSION_GRANTED + && !deniedAccessSystemUserOnlyProvider(callingUserId, + mSystemUserOnly)) { FrameworkStatsLog.write(GET_TYPE_ACCESSED_WITHOUT_PERMISSION, enumCheckUriPermission, callingUid, uri.getAuthority(), type); @@ -865,6 +869,10 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall boolean checkUser(int pid, int uid, Context context) { final int callingUserId = UserHandle.getUserId(uid); + if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) { + return false; + } + if (callingUserId == context.getUserId() || mSingleUser) { return true; } @@ -987,6 +995,9 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall // last chance, check against any uri grants final int callingUserId = UserHandle.getUserId(uid); + if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) { + return PermissionChecker.PERMISSION_HARD_DENIED; + } final Uri userUri = (mSingleUser && !UserHandle.isSameUser(mMyUid, uid)) ? maybeAddUserId(uri, callingUserId) : uri; if (context.checkUriPermission(userUri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION) @@ -2623,6 +2634,7 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall setPathPermissions(info.pathPermissions); mExported = info.exported; mSingleUser = (info.flags & ProviderInfo.FLAG_SINGLE_USER) != 0; + mSystemUserOnly = (info.flags & ProviderInfo.FLAG_SYSTEM_USER_ONLY) != 0; setAuthorities(info.authority); } if (Build.IS_DEBUGGABLE) { @@ -2756,6 +2768,11 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall String auth = uri.getAuthority(); if (!mSingleUser) { int userId = getUserIdFromAuthority(auth, UserHandle.USER_CURRENT); + if (deniedAccessSystemUserOnlyProvider(mContext.getUserId(), + mSystemUserOnly)) { + throw new SecurityException("Trying to query a SYSTEM user only content" + + " provider from user:" + mContext.getUserId()); + } if (userId != UserHandle.USER_CURRENT && userId != mContext.getUserId() // Since userId specified in content uri, the provider userId would be @@ -2929,4 +2946,16 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall Trace.traceBegin(traceTag, methodName + subInfo); } } + /** + * Return true if access to content provider is denied because it's a SYSTEM user only + * provider and the calling user is not the SYSTEM user. + * + * @param callingUserId UserId of the caller accessing the content provider. + * @param systemUserOnly true when the content provider is only available for the SYSTEM user. + */ + private static boolean deniedAccessSystemUserOnlyProvider(int callingUserId, + boolean systemUserOnly) { + return Flags.enableSystemUserOnlyForServicesAndProviders() + && (callingUserId != UserHandle.USER_SYSTEM && systemUserOnly); + } } diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java index 9e553dbfb719..de33fa8b2328 100644 --- a/core/java/android/content/pm/ProviderInfo.java +++ b/core/java/android/content/pm/ProviderInfo.java @@ -89,6 +89,15 @@ public final class ProviderInfo extends ComponentInfo public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000; /** + * Bit in {@link #flags}: If set, this provider will only be available + * for the system user. + * Set from the android.R.attr#systemUserOnly attribute. + * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY} + * @hide + */ + public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY; + + /** * Bit in {@link #flags}: If set, a single instance of the provider will * run for all users on the device. Set from the * {@link android.R.attr#singleUser} attribute. diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index ae46c027505e..2b378b1f09d0 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -101,6 +101,14 @@ public class ServiceInfo extends ComponentInfo public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000; /** + * @hide Bit in {@link #flags}: If set, this service will only be available + * for the system user. + * Set from the android.R.attr#systemUserOnly attribute. + * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY} + */ + public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY; + + /** * Bit in {@link #flags}: If set, a single instance of the service will * run for all users on the device. Set from the * {@link android.R.attr#singleUser} attribute. diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 5bfc012844f8..9644d8095a4d 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -84,4 +84,11 @@ flag { namespace: "profile_experiences" description: "Enable auto-locking private space on device restarts" bug: "296993385" -}
\ No newline at end of file +} +flag { + name: "enable_system_user_only_for_services_and_providers" + namespace: "multiuser" + description: "Enable systemUserOnly manifest attribute for services and providers." + bug: "302354856" + is_fixed_read_only: true +} diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java index 796a57bf6880..2e63664df7aa 100644 --- a/core/java/android/credentials/CredentialManager.java +++ b/core/java/android/credentials/CredentialManager.java @@ -32,6 +32,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.IntentSender; import android.os.Binder; +import android.os.Bundle; import android.os.CancellationSignal; import android.os.IBinder; import android.os.ICancellationSignal; @@ -58,6 +59,9 @@ import java.util.concurrent.Executor; @SystemService(Context.CREDENTIAL_SERVICE) public final class CredentialManager { private static final String TAG = "CredentialManager"; + private static final Bundle OPTIONS_SENDER_BAL_OPTIN = ActivityOptions.makeBasic() + .setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle(); /** @hide */ @IntDef( @@ -757,9 +761,7 @@ public final class CredentialManager { public void onPendingIntent(PendingIntent pendingIntent) { try { mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0, - ActivityOptions.makeBasic() - .setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle()); + OPTIONS_SENDER_BAL_OPTIN); } catch (IntentSender.SendIntentException e) { Log.e( TAG, @@ -817,7 +819,8 @@ public final class CredentialManager { @Override public void onPendingIntent(PendingIntent pendingIntent) { try { - mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); + mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0, + OPTIONS_SENDER_BAL_OPTIN); } catch (IntentSender.SendIntentException e) { Log.e( TAG, diff --git a/core/java/android/credentials/PrepareGetCredentialResponse.java b/core/java/android/credentials/PrepareGetCredentialResponse.java index 212f5716d041..75d671bbb71b 100644 --- a/core/java/android/credentials/PrepareGetCredentialResponse.java +++ b/core/java/android/credentials/PrepareGetCredentialResponse.java @@ -22,9 +22,11 @@ import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.app.ActivityOptions; import android.app.PendingIntent; import android.content.Context; import android.content.IntentSender; +import android.os.Bundle; import android.os.CancellationSignal; import android.os.OutcomeReceiver; import android.util.Log; @@ -41,6 +43,10 @@ import java.util.concurrent.Executor; */ public final class PrepareGetCredentialResponse { + private static final Bundle OPTIONS_SENDER_BAL_OPTIN = ActivityOptions.makeBasic() + .setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle(); + /** * A handle that represents a pending get-credential operation. Pass this handle to {@link * CredentialManager#getCredential(Context, PendingGetCredentialHandle, CancellationSignal, @@ -80,7 +86,8 @@ public final class PrepareGetCredentialResponse { @Override public void onPendingIntent(PendingIntent pendingIntent) { try { - context.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); + context.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0, + OPTIONS_SENDER_BAL_OPTIN); } catch (IntentSender.SendIntentException e) { Log.e(TAG, "startIntentSender() failed for intent for show()", e); executor.execute(() -> callback.onError( @@ -101,7 +108,8 @@ public final class PrepareGetCredentialResponse { }); try { - context.startIntentSender(mPendingIntent.getIntentSender(), null, 0, 0, 0); + context.startIntentSender(mPendingIntent.getIntentSender(), null, 0, 0, 0, + OPTIONS_SENDER_BAL_OPTIN); } catch (IntentSender.SendIntentException e) { Log.e(TAG, "startIntentSender() failed for intent for show()", e); executor.execute(() -> callback.onError( diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index 7b9cb6afd6a0..928604983b70 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -40,6 +40,7 @@ import android.util.Pools.SynchronizedPool; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; +import com.android.text.flags.Flags; import java.lang.ref.WeakReference; @@ -1276,8 +1277,21 @@ public class DynamicLayout extends Layout { } public void onSpanRemoved(Spannable s, Object o, int start, int end) { - if (o instanceof UpdateLayout) - transformAndReflow(s, start, end); + if (o instanceof UpdateLayout) { + if (Flags.insertModeCrashWhenDelete()) { + final DynamicLayout dynamicLayout = mLayout.get(); + if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) { + // It's possible that a Span is removed when the text covering it is + // deleted, in this case, the original start and end of the span might be + // OOB. So it'll reflow the entire string instead. + reflow(s, 0, 0, s.length()); + } else { + reflow(s, start, end - start, end - start); + } + } else { + transformAndReflow(s, start, end); + } + } } public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) { @@ -1287,8 +1301,21 @@ public class DynamicLayout extends Layout { // instead of causing an exception start = 0; } - transformAndReflow(s, start, end); - transformAndReflow(s, nstart, nend); + if (Flags.insertModeCrashWhenDelete()) { + final DynamicLayout dynamicLayout = mLayout.get(); + if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) { + // When text is changed, it'll also trigger onSpanChanged. In this case we + // can't determine the updated range in the transformed text. So it'll + // reflow the entire range instead. + reflow(s, 0, 0, s.length()); + } else { + reflow(s, start, end - start, end - start); + reflow(s, nstart, nend - nstart, nend - nstart); + } + } else { + transformAndReflow(s, start, end); + transformAndReflow(s, nstart, nend); + } } } diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index bf1a59625c93..6e45fea930d2 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -89,3 +89,10 @@ flag { description: "Feature flag for clearing focus when the escape key is pressed." bug: "312921137" } + +flag { + name: "insert_mode_crash_when_delete" + namespace: "text" + description: "A feature flag for fixing the crash while delete text in insert mode." + bug: "314254153" +} diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index c98d1d7ecaea..0f83d58d418d 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -5546,11 +5546,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public static final float REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE = -1; @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) - public static final float REQUESTED_FRAME_RATE_CATEGORY_LOW = -30; + public static final float REQUESTED_FRAME_RATE_CATEGORY_LOW = -2; @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) - public static final float REQUESTED_FRAME_RATE_CATEGORY_NORMAL = -60; + public static final float REQUESTED_FRAME_RATE_CATEGORY_NORMAL = -3; @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) - public static final float REQUESTED_FRAME_RATE_CATEGORY_HIGH = -120; + public static final float REQUESTED_FRAME_RATE_CATEGORY_HIGH = -4; /** * Simple constructor to use when creating a view from code. diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index d12eda35c745..14c53489ba3a 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -1203,11 +1203,7 @@ public abstract class WebSettings { * changes to this setting after that point. * * @param flag {@code true} if the WebView should use the database storage API - * @deprecated WebSQL is deprecated and this method will become a no-op on all - * Android versions once support is removed in Chromium. See - * https://developer.chrome.com/blog/deprecating-web-sql for more information. */ - @Deprecated public abstract void setDatabaseEnabled(boolean flag); /** @@ -1240,11 +1236,7 @@ public abstract class WebSettings { * * @return {@code true} if the database storage API is enabled * @see #setDatabaseEnabled - * @deprecated WebSQL is deprecated and this method will become a no-op on all - * Android versions once support is removed in Chromium. See - * https://developer.chrome.com/blog/deprecating-web-sql for more information. */ - @Deprecated public abstract boolean getDatabaseEnabled(); /** diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index ddcfb40e00ce..57d268ced6f4 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -148,6 +148,7 @@ import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; import com.android.internal.util.Preconditions; import com.android.internal.view.FloatingActionMode; +import com.android.text.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -2343,6 +2344,13 @@ public class Editor { */ void invalidateTextDisplayList(Layout layout, int start, int end) { if (mTextRenderNodes != null && layout instanceof DynamicLayout) { + if (Flags.insertModeCrashWhenDelete() + && mTextView.isOffsetMappingAvailable()) { + // Text is transformed with an OffsetMapping, and we can't know the changed range + // on the transformed text. Invalidate the all display lists instead. + invalidateTextDisplayList(); + return; + } final int startTransformed = mTextView.originalToTransformed(start, OffsetMapping.MAP_STRATEGY_CHARACTER); final int endTransformed = diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java index 5d82d0469d56..12aff1c6669f 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java @@ -29,6 +29,7 @@ import android.content.pm.parsing.result.ParseResult; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.multiuser.Flags; import android.os.Build; import android.os.PatternMatcher; import android.util.Slog; @@ -126,6 +127,10 @@ public class ParsedProviderUtils { .setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SINGLE_USER, R.styleable.AndroidManifestProvider_singleUser, sa)); + if (Flags.enableSystemUserOnlyForServicesAndProviders()) { + provider.setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SYSTEM_USER_ONLY, + R.styleable.AndroidManifestProvider_systemUserOnly, sa)); + } visibleToEphemeral = sa.getBoolean( R.styleable.AndroidManifestProvider_visibleToInstantApps, false); if (visibleToEphemeral) { diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java index a1dd19a3bc90..4ac542f84226 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java @@ -29,6 +29,7 @@ import android.content.pm.parsing.result.ParseResult; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.multiuser.Flags; import android.os.Build; import com.android.internal.R; @@ -105,6 +106,11 @@ public class ParsedServiceUtils { | flag(ServiceInfo.FLAG_SINGLE_USER, R.styleable.AndroidManifestService_singleUser, sa))); + if (Flags.enableSystemUserOnlyForServicesAndProviders()) { + service.setFlags(service.getFlags() | flag(ServiceInfo.FLAG_SYSTEM_USER_ONLY, + R.styleable.AndroidManifestService_systemUserOnly, sa)); + } + visibleToEphemeral = sa.getBoolean( R.styleable.AndroidManifestService_visibleToInstantApps, false); if (visibleToEphemeral) { diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 29086a452ee6..35276bf8ead2 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -506,6 +506,12 @@ receivers, and providers; it can not be used with activities. --> <attr name="singleUser" format="boolean" /> + <!-- If set to true, only a single instance of this component will + run and be available for the SYSTEM user. Non SYSTEM users will not be + allowed to access the component if this flag is enabled. + This flag can be used with services, receivers, providers and activities. --> + <attr name="systemUserOnly" format="boolean" /> + <!-- Specify a specific process that the associated code is to run in. Use with the application tag (to supply a default process for all application components), or with the activity, receiver, service, @@ -2865,6 +2871,7 @@ Context.createAttributionContext() using the first attribution tag contained here. --> <attr name="attributionTags" /> + <attr name="systemUserOnly" format="boolean" /> </declare-styleable> <!-- Attributes that can be supplied in an AndroidManifest.xml @@ -3023,6 +3030,7 @@ ignored when the process is bound into a shared isolated process by a client. --> <attr name="allowSharedIsolatedProcess" format="boolean" /> + <attr name="systemUserOnly" format="boolean" /> </declare-styleable> <!-- @hide The <code>apex-system-service</code> tag declares an apex system service @@ -3150,7 +3158,7 @@ <attr name="uiOptions" /> <attr name="parentActivityName" /> <attr name="singleUser" /> - <!-- @hide This broadcast receiver or activity will only receive broadcasts for the + <!-- This broadcast receiver or activity will only receive broadcasts for the system user--> <attr name="systemUserOnly" format="boolean" /> <attr name="persistableMode" /> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 7d2288599841..b8fc052a2fa9 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -121,6 +121,8 @@ <public name="adServiceTypes" /> <!-- @hide @SystemApi @FlaggedApi("android.content.res.manifest_flagging") --> <public name="featureFlag"/> + <!-- @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") --> + <public name="systemUserOnly"/> </staging-public-group> <staging-public-group type="id" first-id="0x01bc0000"> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 97b1ec713c5b..2de305f8e925 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -639,4 +639,8 @@ applications that come with the platform <permission name="com.android.voicemail.permission.READ_VOICEMAIL"/> <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> + + <privapp-permissions package="com.android.devicediagnostics"> + <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/> + </privapp-permissions> </permissions> diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt new file mode 100644 index 000000000000..4c76168cdeaa --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt @@ -0,0 +1,75 @@ +/* + * 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.wm.shell.bubbles + +import android.content.ComponentName +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewTaskController + +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleTaskViewTest { + + private lateinit var bubbleTaskView: BubbleTaskView + private val context = ApplicationProvider.getApplicationContext<Context>() + + @Before + fun setUp() { + val taskView = TaskView(context, mock<TaskViewTaskController>()) + bubbleTaskView = BubbleTaskView(taskView, directExecutor()) + } + + @Test + fun onTaskCreated_updatesState() { + val componentName = ComponentName(context, "TestClass") + bubbleTaskView.listener.onTaskCreated(123, componentName) + + assertThat(bubbleTaskView.taskId).isEqualTo(123) + assertThat(bubbleTaskView.componentName).isEqualTo(componentName) + assertThat(bubbleTaskView.isCreated).isTrue() + } + + @Test + fun onTaskCreated_callsDelegateListener() { + var actualTaskId = -1 + var actualComponentName: ComponentName? = null + val delegateListener = object : TaskView.Listener { + override fun onTaskCreated(taskId: Int, name: ComponentName) { + actualTaskId = taskId + actualComponentName = name + } + } + bubbleTaskView.delegateListener = delegateListener + + val componentName = ComponentName(context, "TestClass") + bubbleTaskView.listener.onTaskCreated(123, componentName) + + assertThat(actualTaskId).isEqualTo(123) + assertThat(actualComponentName).isEqualTo(componentName) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index f3fe895bf9b4..9f7d0ac9bafe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -74,7 +74,6 @@ import com.android.wm.shell.R; import com.android.wm.shell.common.AlphaOptimizedButton; import com.android.wm.shell.common.TriangleShape; import com.android.wm.shell.taskview.TaskView; -import com.android.wm.shell.taskview.TaskViewTaskController; import java.io.PrintWriter; @@ -146,7 +145,6 @@ public class BubbleExpandedView extends LinearLayout { private AlphaOptimizedButton mManageButton; private TaskView mTaskView; - private TaskViewTaskController mTaskViewTaskController; private BubbleOverflowContainerView mOverflowView; private int mTaskId = INVALID_TASK_ID; @@ -434,7 +432,8 @@ public class BubbleExpandedView extends LinearLayout { * Initialize {@link BubbleController} and {@link BubbleStackView} here, this method must need * to be called after view inflate. */ - void initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow) { + void initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow, + @Nullable BubbleTaskView bubbleTaskView) { mController = controller; mStackView = stackView; mIsOverflow = isOverflow; @@ -451,18 +450,22 @@ public class BubbleExpandedView extends LinearLayout { bringChildToFront(mOverflowView); mManageButton.setVisibility(GONE); } else { - mTaskViewTaskController = new TaskViewTaskController(mContext, - mController.getTaskOrganizer(), - mController.getTaskViewTransitions(), mController.getSyncTransactionQueue()); - mTaskView = new TaskView(mContext, mTaskViewTaskController); - mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener); + mTaskView = bubbleTaskView.getTaskView(); + bubbleTaskView.setDelegateListener(mTaskViewListener); // set a fixed width so it is not recalculated as part of a rotation. the width will be // updated manually after the rotation. FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getContentWidth(), MATCH_PARENT); + if (mTaskView.getParent() != null) { + ((ViewGroup) mTaskView.getParent()).removeView(mTaskView); + } mExpandedViewContainer.addView(mTaskView, lp); bringChildToFront(mTaskView); + if (bubbleTaskView.isCreated()) { + mTaskViewListener.onTaskCreated( + bubbleTaskView.getTaskId(), bubbleTaskView.getComponentName()); + } } } @@ -876,7 +879,7 @@ public class BubbleExpandedView extends LinearLayout { return; } boolean isNew = mBubble == null || didBackingContentChange(bubble); - if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) { + if (isNew || bubble.getKey().equals(mBubble.getKey())) { mBubble = bubble; mManageButton.setContentDescription(getResources().getString( R.string.bubbles_settings_button_description, bubble.getAppName())); @@ -1107,7 +1110,8 @@ public class BubbleExpandedView extends LinearLayout { * has been removed. * * If this view should be reused after this method is called, then - * {@link #initialize(BubbleController, BubbleStackView, boolean)} must be invoked first. + * {@link #initialize(BubbleController, BubbleStackView, boolean, BubbleTaskView)} + * must be invoked first. */ public void cleanUpExpandedState() { if (DEBUG_BUBBLE_EXPANDED_VIEW) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt index 22e836aacfc5..e5d9acedc903 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt @@ -29,6 +29,7 @@ import android.util.PathParser import android.view.LayoutInflater import android.view.View.VISIBLE import android.widget.FrameLayout +import androidx.core.content.ContextCompat import com.android.launcher3.icons.BubbleIconFactory import com.android.wm.shell.R import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView @@ -57,10 +58,16 @@ class BubbleOverflow(private val context: Context, private val positioner: Bubbl /** Call before use and again if cleanUpExpandedState was called. */ fun initialize(controller: BubbleController, forBubbleBar: Boolean) { if (forBubbleBar) { - createBubbleBarExpandedView().initialize(controller, true /* isOverflow */) + createBubbleBarExpandedView() + .initialize(controller, /* isOverflow= */ true, /* bubbleTaskView= */ null) } else { createExpandedView() - .initialize(controller, controller.stackView, true /* isOverflow */) + .initialize( + controller, + controller.stackView, + /* isOverflow= */ true, + /* bubbleTaskView= */ null + ) } } @@ -113,7 +120,10 @@ class BubbleOverflow(private val context: Context, private val positioner: Bubbl context, res.getDimensionPixelSize(R.dimen.bubble_size), res.getDimensionPixelSize(R.dimen.bubble_badge_size), - res.getColor(com.android.launcher3.icons.R.color.important_conversation), + ContextCompat.getColor( + context, + com.android.launcher3.icons.R.color.important_conversation + ), res.getDimensionPixelSize(com.android.internal.R.dimen.importance_ring_stroke_width) ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt new file mode 100644 index 000000000000..2fcd133c7b20 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt @@ -0,0 +1,81 @@ +/* + * 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.wm.shell.bubbles + +import android.app.ActivityTaskManager.INVALID_TASK_ID +import android.content.ComponentName +import androidx.annotation.VisibleForTesting +import com.android.wm.shell.taskview.TaskView +import java.util.concurrent.Executor + +/** + * A wrapper class around [TaskView] for bubble expanded views. + * + * [delegateListener] allows callers to change listeners after a task has been created. + */ +class BubbleTaskView(val taskView: TaskView, executor: Executor) { + + /** Whether the task is already created. */ + var isCreated = false + private set + + /** The task id. */ + var taskId = INVALID_TASK_ID + private set + + /** The component name of the application running in the task. */ + var componentName: ComponentName? = null + private set + + /** [TaskView.Listener] for users of this class. */ + var delegateListener: TaskView.Listener? = null + + /** A [TaskView.Listener] that delegates to [delegateListener]. */ + @get:VisibleForTesting + val listener = object : TaskView.Listener { + override fun onInitialized() { + delegateListener?.onInitialized() + } + + override fun onReleased() { + delegateListener?.onReleased() + } + + override fun onTaskCreated(taskId: Int, name: ComponentName) { + delegateListener?.onTaskCreated(taskId, name) + this@BubbleTaskView.taskId = taskId + isCreated = true + componentName = name + } + + override fun onTaskVisibilityChanged(taskId: Int, visible: Boolean) { + delegateListener?.onTaskVisibilityChanged(taskId, visible) + } + + override fun onTaskRemovalStarted(taskId: Int) { + delegateListener?.onTaskRemovalStarted(taskId) + } + + override fun onBackPressedOnTaskRoot(taskId: Int) { + delegateListener?.onBackPressedOnTaskRoot(taskId) + } + } + + init { + taskView.setListener(executor, listener) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index f6c382fb5b3d..5855a81333d4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -35,10 +35,7 @@ import android.view.View; import androidx.annotation.Nullable; -import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.taskview.TaskView; -import com.android.wm.shell.taskview.TaskViewTaskController; /** * Handles creating and updating the {@link TaskView} associated with a {@link Bubble}. @@ -65,7 +62,6 @@ public class BubbleTaskViewHelper { private final Context mContext; private final BubbleController mController; - private final @ShellMainThread ShellExecutor mMainExecutor; private final BubbleTaskViewHelper.Listener mListener; private final View mParentView; @@ -73,7 +69,6 @@ public class BubbleTaskViewHelper { private Bubble mBubble; @Nullable private PendingIntent mPendingIntent; - private TaskViewTaskController mTaskViewTaskController; @Nullable private TaskView mTaskView; private int mTaskId = INVALID_TASK_ID; @@ -204,17 +199,18 @@ public class BubbleTaskViewHelper { public BubbleTaskViewHelper(Context context, BubbleController controller, BubbleTaskViewHelper.Listener listener, + BubbleTaskView bubbleTaskView, View parent) { mContext = context; mController = controller; - mMainExecutor = mController.getMainExecutor(); mListener = listener; mParentView = parent; - mTaskViewTaskController = new TaskViewTaskController(mContext, - mController.getTaskOrganizer(), - mController.getTaskViewTransitions(), mController.getSyncTransactionQueue()); - mTaskView = new TaskView(mContext, mTaskViewTaskController); - mTaskView.setListener(mMainExecutor, mTaskViewListener); + mTaskView = bubbleTaskView.getTaskView(); + bubbleTaskView.setDelegateListener(mTaskViewListener); + if (bubbleTaskView.isCreated()) { + mTaskId = bubbleTaskView.getTaskId(); + mListener.onTaskCreated(); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java index bb30c5eeebcf..c3d899e7dac7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java @@ -46,6 +46,8 @@ import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; +import com.android.wm.shell.taskview.TaskView; +import com.android.wm.shell.taskview.TaskViewTaskController; import java.lang.ref.WeakReference; import java.util.Objects; @@ -173,10 +175,12 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask BubbleViewInfo info = new BubbleViewInfo(); if (!skipInflation && !b.isInflated()) { + BubbleTaskView bubbleTaskView = createBubbleTaskView(c, controller); LayoutInflater inflater = LayoutInflater.from(c); info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate( R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */); - info.bubbleBarExpandedView.initialize(controller, false /* isOverflow */); + info.bubbleBarExpandedView.initialize( + controller, false /* isOverflow */, bubbleTaskView); } if (!populateCommonInfo(info, c, b, iconFactory)) { @@ -201,9 +205,11 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask R.layout.bubble_view, stackView, false /* attachToRoot */); info.imageView.initialize(controller.getPositioner()); + BubbleTaskView bubbleTaskView = createBubbleTaskView(c, controller); info.expandedView = (BubbleExpandedView) inflater.inflate( R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); - info.expandedView.initialize(controller, stackView, false /* isOverflow */); + info.expandedView.initialize( + controller, stackView, false /* isOverflow */, bubbleTaskView); } if (!populateCommonInfo(info, c, b, iconFactory)) { @@ -219,6 +225,15 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask } return info; } + + private static BubbleTaskView createBubbleTaskView( + Context context, BubbleController controller) { + TaskViewTaskController taskViewTaskController = new TaskViewTaskController(context, + controller.getTaskOrganizer(), + controller.getTaskViewTransitions(), controller.getSyncTransactionQueue()); + TaskView taskView = new TaskView(context, taskViewTaskController); + return new BubbleTaskView(taskView, controller.getMainExecutor()); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 66c0c9640477..3cf23ac114ee 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles.bar; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; + import android.annotation.Nullable; import android.app.ActivityManager; import android.content.Context; @@ -27,6 +29,7 @@ import android.graphics.Rect; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.widget.FrameLayout; @@ -35,6 +38,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleOverflowContainerView; +import com.android.wm.shell.bubbles.BubbleTaskView; import com.android.wm.shell.bubbles.BubbleTaskViewHelper; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.taskview.TaskView; @@ -130,7 +134,8 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView } /** Set the BubbleController on the view, must be called before doing anything else. */ - public void initialize(BubbleController controller, boolean isOverflow) { + public void initialize(BubbleController controller, boolean isOverflow, + @Nullable BubbleTaskView bubbleTaskView) { mController = controller; mIsOverflow = isOverflow; @@ -140,14 +145,19 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView mOverflowView.setBubbleController(mController); addView(mOverflowView); } else { - + mTaskView = bubbleTaskView.getTaskView(); mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, mController, - /* listener= */ this, + /* listener= */ this, bubbleTaskView, /* viewParent= */ this); - mTaskView = mBubbleTaskViewHelper.getTaskView(); - addView(mTaskView); + if (mTaskView.getParent() != null) { + ((ViewGroup) mTaskView.getParent()).removeView(mTaskView); + } + FrameLayout.LayoutParams lp = + new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT); + addView(mTaskView, lp); mTaskView.setEnableSurfaceClipping(true); mTaskView.setCornerRadius(mCornerRadius); + mTaskView.setVisibility(VISIBLE); // Handle view needs to draw on top of task view. bringChildToFront(mHandleView); diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index bba9c9764eee..f84107e8792c 100644 --- a/libs/input/PointerController.cpp +++ b/libs/input/PointerController.cpp @@ -111,7 +111,11 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>& : PointerController( policy, looper, spriteController, enabled, [](const sp<android::gui::WindowInfosListener>& listener) { - SurfaceComposerClient::getDefault()->addWindowInfosListener(listener); + auto initialInfo = std::make_pair(std::vector<android::gui::WindowInfo>{}, + std::vector<android::gui::DisplayInfo>{}); + SurfaceComposerClient::getDefault()->addWindowInfosListener(listener, + &initialInfo); + return initialInfo.second; }, [](const sp<android::gui::WindowInfosListener>& listener) { SurfaceComposerClient::getDefault()->removeWindowInfosListener(listener); @@ -119,8 +123,9 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>& PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, SpriteController& spriteController, - bool enabled, WindowListenerConsumer registerListener, - WindowListenerConsumer unregisterListener) + bool enabled, + const WindowListenerRegisterConsumer& registerListener, + WindowListenerUnregisterConsumer unregisterListener) : mEnabled(enabled), mContext(policy, looper, spriteController, *this), mCursorController(mContext), @@ -128,7 +133,8 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>& mUnregisterWindowInfosListener(std::move(unregisterListener)) { std::scoped_lock lock(getLock()); mLocked.presentation = Presentation::SPOT; - registerListener(mDisplayInfoListener); + const auto& initialDisplayInfos = registerListener(mDisplayInfoListener); + onDisplayInfosChangedLocked(initialDisplayInfos); } PointerController::~PointerController() { diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h index a8b963367f4c..6ee5707622ca 100644 --- a/libs/input/PointerController.h +++ b/libs/input/PointerController.h @@ -79,14 +79,16 @@ public: std::string dump() override; protected: - using WindowListenerConsumer = + using WindowListenerRegisterConsumer = std::function<std::vector<gui::DisplayInfo>( + const sp<android::gui::WindowInfosListener>&)>; + using WindowListenerUnregisterConsumer = std::function<void(const sp<android::gui::WindowInfosListener>&)>; // Constructor used to test WindowInfosListener registration. PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, SpriteController& spriteController, bool enabled, - WindowListenerConsumer registerListener, - WindowListenerConsumer unregisterListener); + const WindowListenerRegisterConsumer& registerListener, + WindowListenerUnregisterConsumer unregisterListener); PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, SpriteController& spriteController, bool enabled); @@ -129,7 +131,7 @@ private: }; sp<DisplayInfoListener> mDisplayInfoListener; - const WindowListenerConsumer mUnregisterWindowInfosListener; + const WindowListenerUnregisterConsumer mUnregisterWindowInfosListener; const ui::Transform& getTransformForDisplayLocked(int displayId) const REQUIRES(getLock()); diff --git a/libs/input/TouchSpotController.cpp b/libs/input/TouchSpotController.cpp index b8de919fbd8c..99952aa14904 100644 --- a/libs/input/TouchSpotController.cpp +++ b/libs/input/TouchSpotController.cpp @@ -93,7 +93,7 @@ void TouchSpotController::setSpots(const PointerCoords* spotCoords, const uint32 const PointerCoords& c = spotCoords[spotIdToIndex[id]]; ALOGD(" spot %d: position=(%0.3f, %0.3f), pressure=%0.3f, displayId=%" PRId32 ".", id, c.getAxisValue(AMOTION_EVENT_AXIS_X), c.getAxisValue(AMOTION_EVENT_AXIS_Y), - c.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE), displayId); + c.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE), mDisplayId); } #endif diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp index adfa91e96ebb..a1bb5b3f1cc4 100644 --- a/libs/input/tests/PointerController_test.cpp +++ b/libs/input/tests/PointerController_test.cpp @@ -160,9 +160,11 @@ public: : PointerController( policy, looper, spriteController, /*enabled=*/true, - [®isteredListener](const sp<android::gui::WindowInfosListener>& listener) { + [®isteredListener](const sp<android::gui::WindowInfosListener>& listener) + -> std::vector<gui::DisplayInfo> { // Register listener registeredListener = listener; + return {}; }, [®isteredListener](const sp<android::gui::WindowInfosListener>& listener) { // Unregister listener diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index c073b79ba5a3..a22fecf3688d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -64,12 +64,11 @@ fun CommunalContainer( transitions = sceneTransitions, ) - // Don't show hub mode UI if keyguard is not present. This is important since we're in the - // shade, which can be opened from many locations. - val isKeyguardShowing by viewModel.isKeyguardVisible.collectAsState(initial = false) + // Don't show hub mode UI if communal is not available. Communal is only available if it has + // been enabled via settings and either keyguard is showing, or, the device is currently + // dreaming. val isCommunalAvailable by viewModel.isCommunalAvailable.collectAsState() - - if (!isKeyguardShowing || !isCommunalAvailable) { + if (!isCommunalAvailable) { return } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 86279ef24ca7..1b7117f41bbb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -124,6 +124,7 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(mainUser) + keyguardRepository.setKeyguardShowing(true) runCurrent() assertThat(isAvailable).isTrue() @@ -150,12 +151,27 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(secondaryUser) + keyguardRepository.setKeyguardShowing(true) runCurrent() assertThat(isAvailable).isFalse() } @Test + fun isCommunalAvailable_whenDreaming_true() = + testScope.runTest { + val isAvailable by collectLastValue(underTest.isCommunalAvailable) + assertThat(isAvailable).isFalse() + + keyguardRepository.setIsEncryptedOrLockdown(false) + userRepository.setSelectedUserInfo(mainUser) + keyguardRepository.setDreaming(true) + runCurrent() + + assertThat(isAvailable).isTrue() + } + + @Test fun updateAppWidgetHostActive_uponStorageUnlockAsMainUser_true() = testScope.runTest { collectLastValue(underTest.isCommunalAvailable) @@ -163,6 +179,7 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(mainUser) + keyguardRepository.setKeyguardShowing(true) runCurrent() assertThat(widgetRepository.isHostActive()).isTrue() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java new file mode 100644 index 000000000000..74c197075461 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java @@ -0,0 +1,101 @@ +/* + * 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.systemui.dreams.touch; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.verify; + +import android.view.GestureDetector; +import android.view.MotionEvent; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.shared.system.InputChannelCompat; +import com.android.systemui.statusbar.NotificationShadeWindowController; +import com.android.systemui.statusbar.phone.CentralSurfaces; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class CommunalTouchHandlerTest extends SysuiTestCase { + @Mock + CentralSurfaces mCentralSurfaces; + @Mock + NotificationShadeWindowController mNotificationShadeWindowController; + @Mock + DreamTouchHandler.TouchSession mTouchSession; + CommunalTouchHandler mTouchHandler; + + private static final int INITIATION_WIDTH = 20; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mTouchHandler = new CommunalTouchHandler( + Optional.of(mCentralSurfaces), + mNotificationShadeWindowController, + INITIATION_WIDTH); + } + + @Test + public void testSessionStartForcesShadeOpen() { + mTouchHandler.onSessionStart(mTouchSession); + verify(mNotificationShadeWindowController).setForcePluginOpen(true, mTouchHandler); + } + + @Test + public void testEventPropagation() { + final MotionEvent motionEvent = Mockito.mock(MotionEvent.class); + + final ArgumentCaptor<InputChannelCompat.InputEventListener> + inputEventListenerArgumentCaptor = + ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class); + + mTouchHandler.onSessionStart(mTouchSession); + verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture()); + inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent); + verify(mCentralSurfaces).handleDreamTouch(motionEvent); + } + + @Test + public void testTouchPilferingOnScroll() { + final MotionEvent motionEvent1 = Mockito.mock(MotionEvent.class); + final MotionEvent motionEvent2 = Mockito.mock(MotionEvent.class); + + final ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerArgumentCaptor = + ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); + + mTouchHandler.onSessionStart(mTouchSession); + verify(mTouchSession).registerGestureListener(gestureListenerArgumentCaptor.capture()); + + assertThat(gestureListenerArgumentCaptor.getValue() + .onScroll(motionEvent1, motionEvent2, 1, 1)) + .isTrue(); + } +} diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 09b02f851c3c..4209c1f6a732 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1805,6 +1805,9 @@ <dimen name="dream_overlay_complication_smartspace_padding">24dp</dimen> <dimen name="dream_overlay_complication_smartspace_max_width">408dp</dimen> + <!-- The width of the swipe target to initiate opening communal hub over dreams. --> + <dimen name="communal_gesture_initiation_width">48dp</dimen> + <!-- The position of the end guide, which dream overlay complications can align their start with if their end is aligned with the parent end. Represented as the percentage over from the start of the parent container. --> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index ec4c7d5bf67e..8ec5ccd7a080 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -227,6 +227,7 @@ <item type="id" name="ambient_indication_container" /> <item type="id" name="status_view_media_container" /> <item type="id" name="smart_space_barrier_bottom" /> + <item type="id" name="weather_clock_date_and_icons_barrier_bottom" /> <!-- Privacy dialog --> <item type="id" name="privacy_dialog_close_app_button" /> diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 44b0383e12c6..4c5871d796b1 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -68,21 +68,23 @@ constructor( private val appWidgetHost: CommunalAppWidgetHost, private val editWidgetsActivityStarter: EditWidgetsActivityStarter ) { - /** Whether communal features are enabled. */ val isCommunalEnabled: Boolean get() = communalRepository.isCommunalEnabled - /** Whether communal features are enabled and available. */ - val isCommunalAvailable: StateFlow<Boolean> = + val isCommunalAvailable = flowOf(isCommunalEnabled) .flatMapLatest { enabled -> if (enabled) combine( keyguardInteractor.isEncryptedOrLockdown, userRepository.selectedUserInfo, - ) { isEncryptedOrLockdown, selectedUserInfo -> - !isEncryptedOrLockdown && selectedUserInfo.isMain + keyguardInteractor.isKeyguardVisible, + keyguardInteractor.isDreaming, + ) { isEncryptedOrLockdown, selectedUserInfo, isKeyguardVisible, isDreaming -> + !isEncryptedOrLockdown && + selectedUserInfo.isMain && + (isKeyguardVisible || isDreaming) } else flowOf(false) } @@ -154,8 +156,6 @@ constructor( it is ObservableCommunalTransitionState.Idle && it.scene == CommunalSceneKey.Communal } - val isKeyguardVisible: Flow<Boolean> = keyguardInteractor.isKeyguardVisible - /** Callback received whenever the [SceneTransitionLayout] finishes a scene transition. */ fun onSceneChanged(newScene: CommunalSceneKey) { communalRepository.setDesiredScene(newScene) diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt index 4e5be9b8aa5e..309c84e4e955 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt @@ -27,12 +27,15 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch /** Encapsulates business-logic related to communal tutorial state. */ @@ -48,7 +51,7 @@ constructor( communalInteractor: CommunalInteractor, ) { /** An observable for whether the tutorial is available. */ - val isTutorialAvailable: Flow<Boolean> = + val isTutorialAvailable: StateFlow<Boolean> = combine( communalInteractor.isCommunalAvailable, keyguardInteractor.isKeyguardVisible, @@ -58,7 +61,11 @@ constructor( isKeyguardVisible && tutorialSettingState != Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED } - .distinctUntilChanged() + .stateIn( + scope = scope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) /** * A flow of the new tutorial state after transitioning. The new state will be calculated based diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt index 4dfc371aaeef..0120b5c87f0a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt @@ -18,7 +18,6 @@ package com.android.systemui.communal.ui.binder import android.widget.TextView -import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle @@ -32,16 +31,14 @@ object CommunalTutorialIndicatorViewBinder { fun bind( view: TextView, viewModel: CommunalTutorialIndicatorViewModel, + isPreviewMode: Boolean = false, ): DisposableHandle { val disposableHandle = view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { launch { - viewModel.showIndicator.collect { isVisible -> - updateView( - view = view, - isIndicatorVisible = isVisible, - ) + viewModel.showIndicator(isPreviewMode).collect { showIndicator -> + view.isVisible = showIndicator } } @@ -51,18 +48,4 @@ object CommunalTutorialIndicatorViewBinder { return disposableHandle } - - private fun updateView( - isIndicatorVisible: Boolean, - view: TextView, - ) { - if (!isIndicatorVisible) { - view.isGone = true - return - } - - if (!view.isVisible) { - view.isVisible = true - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalTutorialIndicatorSection.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalTutorialIndicatorSection.kt index 027cc96350f5..2d9dd50b6d11 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalTutorialIndicatorSection.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalTutorialIndicatorSection.kt @@ -120,6 +120,7 @@ constructor( ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM ) + setVisibility(tutorialIndicatorId, View.GONE) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index acc7981dd460..1e64d3f9cedc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -35,8 +35,6 @@ abstract class BaseCommunalViewModel( ) { val isCommunalAvailable: StateFlow<Boolean> = communalInteractor.isCommunalAvailable - val isKeyguardVisible: Flow<Boolean> = communalInteractor.isKeyguardVisible - val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene /** Whether widgets are currently being re-ordered. */ diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt index 274e61a7499f..63a497213255 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt @@ -20,17 +20,30 @@ import com.android.systemui.communal.domain.interactor.CommunalTutorialInteracto import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged /** View model for communal tutorial indicator on keyguard */ class CommunalTutorialIndicatorViewModel @Inject constructor( - communalTutorialInteractor: CommunalTutorialInteractor, + private val communalTutorialInteractor: CommunalTutorialInteractor, bottomAreaInteractor: KeyguardBottomAreaInteractor, ) { - /** An observable for whether the tutorial indicator view should be visible. */ - val showIndicator: Flow<Boolean> = communalTutorialInteractor.isTutorialAvailable + /** + * An observable for whether the tutorial indicator view should be visible. + * + * @param isPreviewMode Whether for preview keyguard mode in wallpaper settings. + */ + fun showIndicator(isPreviewMode: Boolean): StateFlow<Boolean> { + return if (isPreviewMode) { + MutableStateFlow(false).asStateFlow() + } else { + communalTutorialInteractor.isTutorialAvailable + } + } /** An observable for the alpha level for the tutorial indicator. */ val alpha: Flow<Float> = bottomAreaInteractor.alpha.distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java new file mode 100644 index 000000000000..c9b56a2ebd9a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java @@ -0,0 +1,87 @@ +/* + * 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.systemui.dreams.touch; + +import static com.android.systemui.dreams.touch.dagger.ShadeModule.COMMUNAL_GESTURE_INITIATION_WIDTH; + +import android.graphics.Rect; +import android.graphics.Region; +import android.view.GestureDetector; +import android.view.MotionEvent; + +import com.android.systemui.statusbar.NotificationShadeWindowController; +import com.android.systemui.statusbar.phone.CentralSurfaces; + +import java.util.Optional; + +import javax.inject.Inject; +import javax.inject.Named; + +/** {@link DreamTouchHandler} responsible for handling touches to open communal hub. **/ +public class CommunalTouchHandler implements DreamTouchHandler { + private final int mInitiationWidth; + private final NotificationShadeWindowController mNotificationShadeWindowController; + private final Optional<CentralSurfaces> mCentralSurfaces; + + @Inject + public CommunalTouchHandler( + Optional<CentralSurfaces> centralSurfaces, + NotificationShadeWindowController notificationShadeWindowController, + @Named(COMMUNAL_GESTURE_INITIATION_WIDTH) int initiationWidth) { + mInitiationWidth = initiationWidth; + mCentralSurfaces = centralSurfaces; + mNotificationShadeWindowController = notificationShadeWindowController; + } + + @Override + public void onSessionStart(TouchSession session) { + mCentralSurfaces.ifPresent(surfaces -> handleSessionStart(surfaces, session)); + } + + @Override + public void getTouchInitiationRegion(Rect bounds, Region region) { + final Rect outBounds = new Rect(bounds); + outBounds.inset(outBounds.width() - mInitiationWidth, 0, 0, 0); + region.op(outBounds, Region.Op.UNION); + } + + private void handleSessionStart(CentralSurfaces surfaces, TouchSession session) { + // Force the notification shade window open (otherwise the hub won't show while swiping). + mNotificationShadeWindowController.setForcePluginOpen(true, this); + + session.registerInputListener(ev -> { + surfaces.handleDreamTouch((MotionEvent) ev); + if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) { + var unused = session.pop(); + } + }); + + session.registerGestureListener(new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, + float distanceY) { + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return true; + } + }); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java index 94fe4bd04dab..0f08d376f37c 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java @@ -18,11 +18,13 @@ package com.android.systemui.dreams.touch.dagger; import android.content.res.Resources; -import com.android.systemui.res.R; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dreams.touch.CommunalTouchHandler; import com.android.systemui.dreams.touch.DreamTouchHandler; import com.android.systemui.dreams.touch.ShadeTouchHandler; +import com.android.systemui.res.R; +import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoSet; @@ -33,7 +35,7 @@ import javax.inject.Named; * Dependencies for swipe down to notification over dream. */ @Module -public class ShadeModule { +public abstract class ShadeModule { /** * The height, defined in pixels, of the gesture initiation region at the top of the screen for * swiping down notifications. @@ -41,15 +43,22 @@ public class ShadeModule { public static final String NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT = "notification_shade_gesture_initiation_height"; + /** Width of swipe gesture edge to show communal hub. */ + public static final String COMMUNAL_GESTURE_INITIATION_WIDTH = + "communal_gesture_initiation_width"; + /** * Provides {@link ShadeTouchHandler} to handle notification swipe down over dream. */ - @Provides + @Binds @IntoSet - public static DreamTouchHandler providesNotificationShadeTouchHandler( - ShadeTouchHandler touchHandler) { - return touchHandler; - } + public abstract DreamTouchHandler providesNotificationShadeTouchHandler( + ShadeTouchHandler touchHandler); + + /** Provides {@link CommunalTouchHandler}. */ + @Binds + @IntoSet + public abstract DreamTouchHandler bindCommunalTouchHandler(CommunalTouchHandler touchHandler); /** * Provides the height of the gesture area for notification swipe down. @@ -59,4 +68,13 @@ public class ShadeModule { public static int providesNotificationShadeGestureRegionHeight(@Main Resources resources) { return resources.getDimensionPixelSize(R.dimen.dream_overlay_status_bar_height); } + + /** + * Provides the width of the gesture area for swiping open communal hub. + */ + @Provides + @Named(COMMUNAL_GESTURE_INITIATION_WIDTH) + public static int providesCommunalGestureInitiationWidth(@Main Resources resources) { + return resources.getDimensionPixelSize(R.dimen.communal_gesture_initiation_width); + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 5cebd96249b6..50caf17f71dd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -2507,8 +2507,18 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, String message = ""; switch (msg.what) { case SHOW: - message = "SHOW"; - handleShow((Bundle) msg.obj); + // There is a potential race condition when SysUI starts up. CentralSurfaces + // must invoke #registerCentralSurfaces on this class before any messages can be + // processed. If this happens, repost the message with a small delay and try + // again. + if (mCentralSurfaces == null) { + message = "DELAYING SHOW"; + Message newMsg = mHandler.obtainMessage(SHOW, (Bundle) msg.obj); + mHandler.sendMessageDelayed(newMsg, 100); + } else { + message = "SHOW"; + handleShow((Bundle) msg.obj); + } break; case HIDE: message = "HIDE"; @@ -2595,8 +2605,18 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Trace.endSection(); break; case SYSTEM_READY: - message = "SYSTEM_READY"; - handleSystemReady(); + // There is a potential race condition when SysUI starts up. CentralSurfaces + // must invoke #registerCentralSurfaces on this class before any messages can be + // processed. If this happens, repost the message with a small delay and try + // again. + if (mCentralSurfaces == null) { + message = "DELAYING SYSTEM_READY"; + Message newMsg = mHandler.obtainMessage(SYSTEM_READY); + mHandler.sendMessageDelayed(newMsg, 100); + } else { + message = "SYSTEM_READY"; + handleSystemReady(); + } break; } Log.d(TAG, "KeyguardViewMediator queue processing message: " + message); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index 9d6b30f9813d..dd2b9d43c078 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -62,6 +62,7 @@ constructor( listenForHubToDozing() listenForHubToPrimaryBouncer() listenForHubToAlternateBouncer() + listenForHubToGone() } override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator { @@ -129,6 +130,18 @@ constructor( } } + private fun listenForHubToGone() { + scope.launch { + keyguardInteractor.isKeyguardGoingAway + .sample(startedKeyguardTransitionStep, ::Pair) + .collect { (isKeyguardGoingAway, lastStartedStep) -> + if (isKeyguardGoingAway && lastStartedStep.to == fromState) { + startTransitionTo(KeyguardState.GONE) + } + } + } + } + companion object { const val TAG = "FromGlanceableHubTransitionInteractor" val DEFAULT_DURATION = 400.milliseconds diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index 742790eeaedb..7477624f52d8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator import com.android.app.animation.Interpolators +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -25,6 +26,7 @@ import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.util.kotlin.Utils.Companion.sample import com.android.systemui.util.kotlin.Utils.Companion.toTriple import com.android.systemui.util.kotlin.sample import javax.inject.Inject @@ -45,6 +47,7 @@ constructor( @Main mainDispatcher: CoroutineDispatcher, private val keyguardInteractor: KeyguardInteractor, private val powerInteractor: PowerInteractor, + private val communalInteractor: CommunalInteractor, ) : TransitionInteractor( fromState = KeyguardState.GONE, @@ -56,18 +59,27 @@ constructor( override fun start() { listenForGoneToAodOrDozing() listenForGoneToDreaming() - listenForGoneToLockscreen() + listenForGoneToLockscreenOrHub() listenForGoneToDreamingLockscreenHosted() } // Primarily for when the user chooses to lock down the device - private fun listenForGoneToLockscreen() { + private fun listenForGoneToLockscreenOrHub() { scope.launch { keyguardInteractor.isKeyguardShowing - .sample(startedKeyguardTransitionStep, ::Pair) - .collect { (isKeyguardShowing, lastStartedStep) -> + .sample( + startedKeyguardTransitionStep, + communalInteractor.isIdleOnCommunal, + ) + .collect { (isKeyguardShowing, lastStartedStep, isIdleOnCommunal) -> if (isKeyguardShowing && lastStartedStep.to == KeyguardState.GONE) { - startTransitionTo(KeyguardState.LOCKSCREEN) + val to = + if (isIdleOnCommunal) { + KeyguardState.GLANCEABLE_HUB + } else { + KeyguardState.LOCKSCREEN + } + startTransitionTo(to) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt index 3dd3e0762c7c..8d6493f9a278 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt @@ -24,6 +24,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.BaseBlueprintTransition import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel import com.android.systemui.lifecycle.repeatWhenAttached @@ -53,7 +54,8 @@ class KeyguardBlueprintViewBinder { } // Apply transition. - if (prevBluePrint != null && prevBluePrint != blueprint) { + if (!keyguardBottomAreaRefactor() && prevBluePrint != null && + prevBluePrint != blueprint) { TransitionManager.beginDelayedTransition( constraintLayout, BaseBlueprintTransition() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt index 400b8bfff9b0..3c3ebdfc066b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt @@ -89,6 +89,12 @@ object KeyguardClockViewBinder { } } } + launch { + if (!migrateClocksToBlueprint()) return@launch + viewModel.isAodIconsVisible.collect { + applyConstraints(clockSection, keyguardRootView, true) + } + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index eb3afb7c9eec..841bad4c15cc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -39,6 +39,7 @@ import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.widget.FrameLayout +import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isInvisible import com.android.keyguard.ClockEventController @@ -48,6 +49,8 @@ import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.communal.ui.binder.CommunalTutorialIndicatorViewBinder +import com.android.systemui.communal.ui.viewmodel.CommunalTutorialIndicatorViewModel import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -133,6 +136,7 @@ constructor( private val screenOffAnimationController: ScreenOffAnimationController, private val shadeInteractor: ShadeInteractor, private val secureSettings: SecureSettings, + private val communalTutorialViewModel: CommunalTutorialIndicatorViewModel, ) { val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN) private val width: Int = bundle.getInt(KEY_VIEW_WIDTH) @@ -408,6 +412,8 @@ constructor( smartSpaceView?.let { KeyguardPreviewSmartspaceViewBinder.bind(it, smartspaceViewModel) } + + setupCommunalTutorialIndicator(keyguardRootView) } ) } @@ -601,6 +607,17 @@ constructor( } } + private fun setupCommunalTutorialIndicator(keyguardRootView: ConstraintLayout) { + keyguardRootView.findViewById<TextView>(R.id.communal_tutorial_indicator)?.let { + indicatorView -> + CommunalTutorialIndicatorViewBinder.bind( + indicatorView, + communalTutorialViewModel, + isPreviewMode = true, + ) + } + } + private suspend fun fetchThemeStyleFromSetting(): Style { val overlayPackageJson = withContext(backgroundDispatcher) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index ed7abff555e7..bc6c7cbf35fb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -57,7 +57,6 @@ constructor( private var nicBindingDisposable: DisposableHandle? = null private val nicId = R.id.aod_notification_icon_container private lateinit var nic: NotificationIconContainer - private val smartSpaceBarrier = View.generateViewId() override fun addViews(constraintLayout: ConstraintLayout) { if (!KeyguardShadeMigrationNssl.isEnabled) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index b344d3b9afea..a1b3f270f642 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import android.view.View +import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM @@ -35,6 +36,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardClockViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFaceLayout import com.android.systemui.res.R @@ -61,6 +63,7 @@ constructor( protected val keyguardClockViewModel: KeyguardClockViewModel, private val context: Context, private val splitShadeStateController: SplitShadeStateController, + val smartspaceViewModel: KeyguardSmartspaceViewModel, val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>, ) : KeyguardSection() { override fun addViews(constraintLayout: ConstraintLayout) {} @@ -117,6 +120,35 @@ constructor( private fun getLargeClockFace(clock: ClockController): ClockFaceLayout = clock.largeClock.layout private fun getSmallClockFace(clock: ClockController): ClockFaceLayout = clock.smallClock.layout + + fun constrainWeatherClockDateIconsBarrier(constraints: ConstraintSet) { + constraints.apply { + if (keyguardClockViewModel.isAodIconsVisible.value) { + createBarrier( + R.id.weather_clock_date_and_icons_barrier_bottom, + Barrier.BOTTOM, + 0, + *intArrayOf(sharedR.id.bc_smartspace_view, R.id.aod_notification_icon_container) + ) + } else { + if (smartspaceViewModel.bcSmartspaceVisibility.value == VISIBLE) { + createBarrier( + R.id.weather_clock_date_and_icons_barrier_bottom, + Barrier.BOTTOM, + 0, + (sharedR.id.bc_smartspace_view) + ) + } else { + createBarrier( + R.id.weather_clock_date_and_icons_barrier_bottom, + Barrier.BOTTOM, + getDimen(ENHANCED_SMARTSPACE_HEIGHT), + (R.id.lockscreen_clock_view) + ) + } + } + } + } open fun applyDefaultConstraints(constraints: ConstraintSet) { val guideline = if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID @@ -173,6 +205,8 @@ constructor( } connect(R.id.lockscreen_clock_view, TOP, PARENT_ID, TOP, smallClockTopMargin) } + + constrainWeatherClockDateIconsBarrier(constraints) } private fun getDimen(name: String): Int { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt index f37d9f801db3..6763e0a1b798 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt @@ -27,6 +27,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.SettingsClockSize import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.Utils import javax.inject.Inject @@ -44,6 +45,7 @@ constructor( private val keyguardClockInteractor: KeyguardClockInteractor, @Application private val applicationScope: CoroutineScope, private val splitShadeStateController: SplitShadeStateController, + notifsKeyguardInteractor: NotificationsKeyguardInteractor, ) { var burnInLayer: Layer? = null val useLargeClock: Boolean @@ -91,6 +93,13 @@ constructor( initialValue = false ) + val isAodIconsVisible: StateFlow<Boolean> = + notifsKeyguardInteractor.areNotificationsFullyHidden.stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false + ) + // Needs to use a non application context to get display cutout. fun getSmallClockTopMargin(context: Context) = if (splitShadeStateController.shouldUseSplitNotificationShade(context.resources)) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 8c852cd04738..863bb36e4ed0 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -272,6 +272,14 @@ public class NotificationShadeWindowViewController implements Dumpable { return result; } + /** + * Handle a touch event while dreaming by forwarding the event to the content view. + * @param event The event to forward. + */ + public void handleDreamTouch(MotionEvent event) { + mView.dispatchTouchEvent(event); + } + /** Inflates the {@link R.layout#status_bar_expanded} layout and sets it up. */ public void setupExpandedStatusBar() { mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index 909cff37b67d..e5982428fe14 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -542,7 +542,19 @@ public final class KeyboardShortcutListSearch { new ShortcutKeyGroupMultiMappingInfo( context.getString(R.string.group_system_access_google_assistant), Arrays.asList( - Pair.create(KeyEvent.KEYCODE_A, KeyEvent.META_META_ON))) + Pair.create(KeyEvent.KEYCODE_A, KeyEvent.META_META_ON))), + /* Lock screen: Meta + L */ + new ShortcutKeyGroupMultiMappingInfo( + context.getString(R.string.group_system_lock_screen), + Arrays.asList( + Pair.create(KeyEvent.KEYCODE_L, KeyEvent.META_META_ON))), + /* Pull up Notes app for quick memo: Meta + Ctrl + N */ + new ShortcutKeyGroupMultiMappingInfo( + context.getString(R.string.group_system_quick_memo), + Arrays.asList( + Pair.create( + KeyEvent.KEYCODE_N, + KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON))) ); for (ShortcutKeyGroupMultiMappingInfo info : infoList) { systemGroup.addItem(info.getShortcutMultiMappingInfo()); @@ -584,11 +596,17 @@ public final class KeyboardShortcutListSearch { new ArrayList<>()); // System multitasking shortcuts: + // Enter Split screen with current app to RHS: Meta + Ctrl + Right arrow + // Enter Split screen with current app to LHS: Meta + Ctrl + Left arrow // Switch from Split screen to full screen: Meta + Ctrl + Up arrow String[] shortcutLabels = { + context.getString(R.string.system_multitasking_rhs), + context.getString(R.string.system_multitasking_lhs), context.getString(R.string.system_multitasking_full_screen), }; int[] keyCodes = { + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_UP, }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 90cba409a787..40194361e12b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -25,6 +25,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.UserHandle; +import android.view.MotionEvent; import android.view.RemoteAnimationAdapter; import android.view.View; import android.window.RemoteTransition; @@ -277,6 +278,13 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { void awakenDreams(); + /** + * Handle a touch event while dreaming when the touch was initiated within a prescribed + * swipeable area. This method is provided for cases where swiping in certain areas of a dream + * should be handled by CentralSurfaces instead (e.g. swiping communal hub open). + */ + void handleDreamTouch(MotionEvent event); + boolean isBouncerShowing(); boolean isBouncerShowingScrimmed(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt index 7dc4b96ea154..60dfaa790ec9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.phone import android.content.Intent +import android.view.MotionEvent import androidx.lifecycle.LifecycleRegistry import com.android.keyguard.AuthKeyguardMessageArea import com.android.systemui.animation.ActivityLaunchAnimator @@ -78,6 +79,7 @@ abstract class CentralSurfacesEmptyImpl : CentralSurfaces { override fun updateScrimController() {} override fun shouldIgnoreTouch() = false override fun isDeviceInteractive() = false + override fun handleDreamTouch(event: MotionEvent?) {} override fun awakenDreams() {} override fun isBouncerShowing() = false override fun isBouncerShowingScrimmed() = false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 6e3aabf7c754..266c19c09941 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -80,6 +80,7 @@ import android.util.Log; import android.view.Display; import android.view.IRemoteAnimationRunner; import android.view.IWindowManager; +import android.view.MotionEvent; import android.view.ThreadedRenderer; import android.view.View; import android.view.WindowInsets; @@ -2903,6 +2904,11 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { }; @Override + public void handleDreamTouch(MotionEvent event) { + getNotificationShadeWindowViewController().handleDreamTouch(event); + } + + @Override public void awakenDreams() { mUiBgExecutor.execute(() -> { try { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index fe49c0791370..6b303263d4b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.content.Context +import com.android.internal.telephony.flags.Flags import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.graph.SignalDrawable import com.android.settingslib.mobile.MobileIconCarrierIdOverrides @@ -32,14 +33,18 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIc import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.DefaultIcon import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.OverriddenIcon import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -79,6 +84,9 @@ interface MobileIconInteractor { /** Whether or not to show the slice attribution */ val showSliceAttribution: StateFlow<Boolean> + /** True if this connection is satellite-based */ + val isNonTerrestrial: StateFlow<Boolean> + /** * Provider name for this network connection. The name can be one of 3 values: * 1. The default network name, if one is configured @@ -244,6 +252,13 @@ class MobileIconInteractorImpl( override val showSliceAttribution: StateFlow<Boolean> = connectionRepository.hasPrioritizedNetworkCapabilities + override val isNonTerrestrial: StateFlow<Boolean> = + if (Flags.carrierEnabledSatelliteFlag()) { + connectionRepository.isNonTerrestrial + } else { + MutableStateFlow(false).asStateFlow() + } + override val isRoaming: StateFlow<Boolean> = combine( connectionRepository.carrierNetworkChangeActive, @@ -313,26 +328,45 @@ class MobileIconInteractorImpl( } .stateIn(scope, SharingStarted.WhileSubscribed(), 0) - override val signalLevelIcon: StateFlow<SignalIconModel> = run { - val initial = - SignalIconModel( - level = shownLevel.value, - numberOfLevels = numberOfLevels.value, - showExclamationMark = showExclamationMark.value, - carrierNetworkChange = carrierNetworkChangeActive.value, - ) + private val cellularIcon: Flow<SignalIconModel.Cellular> = combine( + shownLevel, + numberOfLevels, + showExclamationMark, + carrierNetworkChangeActive, + ) { shownLevel, numberOfLevels, showExclamationMark, carrierNetworkChange -> + SignalIconModel.Cellular( shownLevel, numberOfLevels, showExclamationMark, - carrierNetworkChangeActive, - ) { shownLevel, numberOfLevels, showExclamationMark, carrierNetworkChange -> - SignalIconModel( - shownLevel, - numberOfLevels, - showExclamationMark, - carrierNetworkChange, - ) + carrierNetworkChange, + ) + } + + private val satelliteIcon: Flow<SignalIconModel.Satellite> = + shownLevel.map { + SignalIconModel.Satellite( + level = it, + icon = SatelliteIconModel.fromSignalStrength(it) + ?: SatelliteIconModel.fromSignalStrength(0)!! + ) + } + + override val signalLevelIcon: StateFlow<SignalIconModel> = run { + val initial = + SignalIconModel.Cellular( + shownLevel.value, + numberOfLevels.value, + showExclamationMark.value, + carrierNetworkChangeActive.value, + ) + isNonTerrestrial + .flatMapLatest { ntn -> + if (ntn) { + satelliteIcon + } else { + cellularIcon + } } .distinctUntilChanged() .logDiffsForTable( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt index e58f08183157..d6b8fd4fec87 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt @@ -17,51 +17,94 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.model import com.android.settingslib.graph.SignalDrawable +import com.android.systemui.common.shared.model.Icon import com.android.systemui.log.table.Diffable import com.android.systemui.log.table.TableRowLogger -/** A model that will be consumed by [SignalDrawable] to show the mobile triangle icon. */ -data class SignalIconModel( - val level: Int, - val numberOfLevels: Int, - val showExclamationMark: Boolean, - val carrierNetworkChange: Boolean, -) : Diffable<SignalIconModel> { - // TODO(b/267767715): Can we implement [logDiffs] and [logFull] generically for data classes? +sealed interface SignalIconModel : Diffable<SignalIconModel> { + val level: Int + override fun logDiffs(prevVal: SignalIconModel, row: TableRowLogger) { - if (prevVal.level != level) { - row.logChange(COL_LEVEL, level) + logPartial(prevVal, row) + } + + override fun logFull(row: TableRowLogger) = logFully(row) + + fun logFully(row: TableRowLogger) + + fun logPartial(prevVal: SignalIconModel, row: TableRowLogger) + + /** A model that will be consumed by [SignalDrawable] to show the mobile triangle icon. */ + data class Cellular( + override val level: Int, + val numberOfLevels: Int, + val showExclamationMark: Boolean, + val carrierNetworkChange: Boolean, + ) : SignalIconModel { + override fun logPartial(prevVal: SignalIconModel, row: TableRowLogger) { + if (prevVal !is Cellular) { + logFull(row) + } else { + if (prevVal.level != level) { + row.logChange(COL_LEVEL, level) + } + if (prevVal.numberOfLevels != numberOfLevels) { + row.logChange(COL_NUM_LEVELS, numberOfLevels) + } + if (prevVal.showExclamationMark != showExclamationMark) { + row.logChange(COL_SHOW_EXCLAMATION, showExclamationMark) + } + if (prevVal.carrierNetworkChange != carrierNetworkChange) { + row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChange) + } + } } - if (prevVal.numberOfLevels != numberOfLevels) { + + override fun logFully(row: TableRowLogger) { + row.logChange(COL_TYPE, "c") + row.logChange(COL_LEVEL, level) row.logChange(COL_NUM_LEVELS, numberOfLevels) - } - if (prevVal.showExclamationMark != showExclamationMark) { row.logChange(COL_SHOW_EXCLAMATION, showExclamationMark) - } - if (prevVal.carrierNetworkChange != carrierNetworkChange) { row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChange) } - } - override fun logFull(row: TableRowLogger) { - row.logChange(COL_LEVEL, level) - row.logChange(COL_NUM_LEVELS, numberOfLevels) - row.logChange(COL_SHOW_EXCLAMATION, showExclamationMark) - row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChange) + /** Convert this model to an [Int] consumable by [SignalDrawable]. */ + fun toSignalDrawableState(): Int = + if (carrierNetworkChange) { + SignalDrawable.getCarrierChangeState(numberOfLevels) + } else { + SignalDrawable.getState(level, numberOfLevels, showExclamationMark) + } } - /** Convert this model to an [Int] consumable by [SignalDrawable]. */ - fun toSignalDrawableState(): Int = - if (carrierNetworkChange) { - SignalDrawable.getCarrierChangeState(numberOfLevels) - } else { - SignalDrawable.getState(level, numberOfLevels, showExclamationMark) + /** + * For non-terrestrial networks, we can use a resource-backed icon instead of the + * [SignalDrawable]-backed version above + */ + data class Satellite( + override val level: Int, + val icon: Icon.Resource, + ) : SignalIconModel { + override fun logPartial(prevVal: SignalIconModel, row: TableRowLogger) { + if (prevVal !is Satellite) { + logFull(row) + } else { + if (prevVal.level != level) row.logChange(COL_LEVEL, level) + } } + override fun logFully(row: TableRowLogger) { + row.logChange("numLevels", "HELLO") + row.logChange(COL_TYPE, "s") + row.logChange(COL_LEVEL, level) + } + } + companion object { private const val COL_LEVEL = "level" private const val COL_NUM_LEVELS = "numLevels" private const val COL_SHOW_EXCLAMATION = "showExclamation" private const val COL_CARRIER_NETWORK_CHANGE = "carrierNetworkChange" + private const val COL_TYPE = "type" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt index a1a5370819f8..43cb38ff0108 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt @@ -59,7 +59,7 @@ constructor( str1 = parentView.getIdForLogging() int1 = subId int2 = icon.level - bool1 = icon.showExclamationMark + bool1 = if (icon is SignalIconModel.Cellular) icon.showExclamationMark else false }, { "Binder[subId=$int1, viewId=$str1] received new signal icon: " + diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index 5475528152ed..a0c5618041f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -38,6 +38,7 @@ import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding @@ -70,7 +71,7 @@ object MobileIconBinder { val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type) val networkTypeContainer = view.requireViewById<FrameLayout>(R.id.mobile_type_container) val iconView = view.requireViewById<ImageView>(R.id.mobile_signal) - val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) } + val mobileDrawable = SignalDrawable(view.context) val roamingView = view.requireViewById<ImageView>(R.id.mobile_roaming) val roamingSpace = view.requireViewById<Space>(R.id.mobile_roaming_space) val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot) @@ -138,7 +139,12 @@ object MobileIconBinder { viewModel.subscriptionId, icon, ) - mobileDrawable.level = icon.toSignalDrawableState() + if (icon is SignalIconModel.Cellular) { + iconView.setImageDrawable(mobileDrawable) + mobileDrawable.level = icon.toSignalDrawableState() + } else if (icon is SignalIconModel.Satellite) { + IconViewBinder.bind(icon.icon, iconView) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index 60c662da6aec..eda5c44b5c2f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -33,12 +33,15 @@ import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityMod import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn /** Common interface for all of the location-based mobile icon view models. */ @@ -71,7 +74,6 @@ interface MobileIconViewModelCommon { * model gets the exact same information, as well as allows us to log that unified state only once * per icon. */ -@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) class MobileIconViewModel( override val subscriptionId: Int, @@ -81,6 +83,100 @@ class MobileIconViewModel( flags: FeatureFlagsClassic, scope: CoroutineScope, ) : MobileIconViewModelCommon { + private val cellProvider by lazy { + CellularIconViewModel( + subscriptionId, + iconInteractor, + airplaneModeInteractor, + constants, + flags, + scope, + ) + } + + private val satelliteProvider by lazy { + CarrierBasedSatelliteViewModelImpl( + subscriptionId, + iconInteractor, + ) + } + + /** + * Similar to repository switching, this allows us to split up the logic of satellite/cellular + * states, since they are different by nature + */ + private val vmProvider: Flow<MobileIconViewModelCommon> = + iconInteractor.isNonTerrestrial + .mapLatest { nonTerrestrial -> + if (nonTerrestrial) { + satelliteProvider + } else { + cellProvider + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), cellProvider) + + override val isVisible: StateFlow<Boolean> = + vmProvider + .flatMapLatest { it.isVisible } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val icon: Flow<SignalIconModel> = vmProvider.flatMapLatest { it.icon } + + override val contentDescription: Flow<ContentDescription> = + vmProvider.flatMapLatest { it.contentDescription } + + override val roaming: Flow<Boolean> = vmProvider.flatMapLatest { it.roaming } + + override val networkTypeIcon: Flow<Icon.Resource?> = + vmProvider.flatMapLatest { it.networkTypeIcon } + + override val networkTypeBackground: StateFlow<Icon.Resource?> = + vmProvider + .flatMapLatest { it.networkTypeBackground } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) + + override val activityInVisible: Flow<Boolean> = + vmProvider.flatMapLatest { it.activityInVisible } + + override val activityOutVisible: Flow<Boolean> = + vmProvider.flatMapLatest { it.activityOutVisible } + + override val activityContainerVisible: Flow<Boolean> = + vmProvider.flatMapLatest { it.activityContainerVisible } +} + +/** Representation of this network when it is non-terrestrial (e.g., satellite) */ +private class CarrierBasedSatelliteViewModelImpl( + override val subscriptionId: Int, + interactor: MobileIconInteractor, +) : MobileIconViewModelCommon { + override val isVisible: StateFlow<Boolean> = MutableStateFlow(true) + override val icon: Flow<SignalIconModel> = interactor.signalLevelIcon + + override val contentDescription: Flow<ContentDescription> = + MutableStateFlow(ContentDescription.Loaded("")) + + /** These fields are not used for satellite icons currently */ + override val roaming: Flow<Boolean> = flowOf(false) + override val networkTypeIcon: Flow<Icon.Resource?> = flowOf(null) + override val networkTypeBackground: StateFlow<Icon.Resource?> = MutableStateFlow(null) + override val activityInVisible: Flow<Boolean> = flowOf(false) + override val activityOutVisible: Flow<Boolean> = flowOf(false) + override val activityContainerVisible: Flow<Boolean> = flowOf(false) +} + +/** Terrestrial (cellular) icon. */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +private class CellularIconViewModel( + override val subscriptionId: Int, + iconInteractor: MobileIconInteractor, + airplaneModeInteractor: AirplaneModeInteractor, + constants: ConnectivityConstants, + flags: FeatureFlagsClassic, + scope: CoroutineScope, +) : MobileIconViewModelCommon { override val isVisible: StateFlow<Boolean> = if (!constants.hasDataCapabilities) { flowOf(false) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt index 6938d667ca81..63566ee814ab 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt @@ -28,7 +28,7 @@ object SatelliteIconModel { fun fromConnectionState( connectionState: SatelliteConnectionState, signalStrength: Int, - ): Icon? = + ): Icon.Resource? = when (connectionState) { // TODO(b/316635648): check if this should be null SatelliteConnectionState.Unknown, @@ -41,9 +41,13 @@ object SatelliteIconModel { SatelliteConnectionState.Connected -> fromSignalStrength(signalStrength) } - private fun fromSignalStrength( + /** + * Satellite icon appropriate for when we are connected. Use [fromConnectionState] for a more + * generally correct representation. + */ + fun fromSignalStrength( signalStrength: Int, - ): Icon? = + ): Icon.Resource? = // TODO(b/316634365): these need content descriptions when (signalStrength) { // No signal diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt index ae58398753e8..352413ee568a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel import android.content.Context import android.text.Html import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -28,6 +29,7 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileModel import com.android.systemui.statusbar.pipeline.shared.ui.model.SignalIcon @@ -116,14 +118,31 @@ constructor( it.signalLevelIcon, mobileDataContentName, ) { networkNameModel, signalIcon, dataContentDescription -> - val secondary = - mobileDataContentConcat(networkNameModel.name, dataContentDescription) - InternetTileModel.Active( - secondaryTitle = secondary, - icon = SignalIcon(signalIcon.toSignalDrawableState()), - stateDescription = ContentDescription.Loaded(secondary.toString()), - contentDescription = ContentDescription.Loaded(internetLabel), - ) + when (signalIcon) { + is SignalIconModel.Cellular -> { + val secondary = + mobileDataContentConcat( + networkNameModel.name, + dataContentDescription + ) + InternetTileModel.Active( + secondaryTitle = secondary, + icon = SignalIcon(signalIcon.toSignalDrawableState()), + stateDescription = ContentDescription.Loaded(secondary.toString()), + contentDescription = ContentDescription.Loaded(internetLabel), + ) + } + is SignalIconModel.Satellite -> { + val secondary = + signalIcon.icon.contentDescription.loadContentDescription(context) + InternetTileModel.Active( + secondaryTitle = secondary, + iconId = signalIcon.icon.res, + stateDescription = ContentDescription.Loaded(secondary), + contentDescription = ContentDescription.Loaded(internetLabel), + ) + } + } } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 754a7fd81475..8a3a4342915b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -309,6 +309,28 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testRaceCondition_doNotRegisterCentralSurfacesImmediately() { + create(false); + + // GIVEN central surfaces is not registered with KeyguardViewMediator, but a call to enable + // keyguard comes in + mViewMediator.onSystemReady(); + mViewMediator.setKeyguardEnabled(true); + TestableLooper.get(this).processAllMessages(); + + // If this step has been reached, then system ui has not crashed. Now register + // CentralSurfaces + assertFalse(mViewMediator.isShowingAndNotOccluded()); + register(); + TestableLooper.get(this).moveTimeForward(100); + TestableLooper.get(this).processAllMessages(); + + // THEN keyguard is shown + assertTrue(mViewMediator.isShowingAndNotOccluded()); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) public void onLockdown_showKeyguard_evenIfKeyguardIsNotEnabledExternally() { // GIVEN keyguard is not enabled and isn't showing mViewMediator.onSystemReady(); @@ -1139,6 +1161,11 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { } private void createAndStartViewMediator(boolean orderUnlockAndWake) { + create(orderUnlockAndWake); + register(); + } + + private void create(boolean orderUnlockAndWake) { mContext.getOrCreateTestableResources().addOverride( com.android.internal.R.bool.config_orderUnlockAndWake, orderUnlockAndWake); @@ -1189,7 +1216,9 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mSelectedUserInteractor, mKeyguardInteractor); mViewMediator.start(); + } + private void register() { mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null, null); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index 892be354180a..1e2b6fadf048 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -244,6 +244,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, powerInteractor = powerInteractor, + communalInteractor = communalInteractor, ) .apply { start() } @@ -906,6 +907,37 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { } @Test + fun goneToGlanceableHub() = + testScope.runTest { + // GIVEN a prior transition has run to GONE + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GONE) + + // GIVEN the device is idle on the glanceable hub + val idleTransitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal) + ) + communalInteractor.setTransitionState(idleTransitionState) + runCurrent() + + // WHEN the keyguard starts to show + keyguardRepository.setKeyguardShowing(true) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(transitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo(FromGoneTransitionInteractor::class.simpleName) + assertThat(info.from).isEqualTo(KeyguardState.GONE) + assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test fun alternateBouncerToPrimaryBouncer() = testScope.runTest { // GIVEN a prior transition has run to ALTERNATE_BOUNCER @@ -1764,6 +1796,30 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { coroutineContext.cancelChildren() } + @Test + fun glanceableHubToGone() = + testScope.runTest { + // GIVEN a prior transition has run to GLANCEABLE_HUB + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB) + + // WHEN keyguard goes away + keyguardRepository.setKeyguardGoingAway(true) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(transitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName) + .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName) + assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB) + assertThat(info.to).isEqualTo(KeyguardState.GONE) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + private fun createKeyguardInteractor(): KeyguardInteractor { return KeyguardInteractorFactory.create( featureFlags = featureFlags, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt index 57b555989166..acb6ff0192e8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt @@ -19,12 +19,15 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.pm.PackageManager import android.content.res.Resources +import android.view.View.GONE +import android.view.View.VISIBLE import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.Utils @@ -49,8 +52,11 @@ class ClockSectionTest : SysuiTestCase() { @Mock private lateinit var keyguardClockInteractor: KeyguardClockInteractor @Mock private lateinit var keyguardClockViewModel: KeyguardClockViewModel @Mock private lateinit var splitShadeStateController: SplitShadeStateController + @Mock private lateinit var smartspaceViewModel: KeyguardSmartspaceViewModel @Mock private lateinit var blueprintInteractor: Lazy<KeyguardBlueprintInteractor> + private val bcSmartspaceVisibility: MutableStateFlow<Int> = MutableStateFlow(VISIBLE) private val clockShouldBeCentered: MutableStateFlow<Boolean> = MutableStateFlow(true) + private val isAodIconsVisible: MutableStateFlow<Boolean> = MutableStateFlow(true) private lateinit var underTest: ClockSection @@ -110,6 +116,8 @@ class ClockSectionTest : SysuiTestCase() { mContext.setMockPackageManager(packageManager) whenever(keyguardClockViewModel.clockShouldBeCentered).thenReturn(clockShouldBeCentered) + whenever(keyguardClockViewModel.isAodIconsVisible).thenReturn(isAodIconsVisible) + whenever(smartspaceViewModel.bcSmartspaceVisibility).thenReturn(bcSmartspaceVisibility) underTest = ClockSection( @@ -117,6 +125,7 @@ class ClockSectionTest : SysuiTestCase() { keyguardClockViewModel, mContext, splitShadeStateController, + smartspaceViewModel, blueprintInteractor ) } @@ -176,6 +185,40 @@ class ClockSectionTest : SysuiTestCase() { assetSmallClockTop(cs, expectedSmallClockTopMargin) } + @Test + fun testSmartspaceVisible_weatherClockDateAndIconsBarrierBottomBelowBCSmartspace() { + isAodIconsVisible.value = false + bcSmartspaceVisibility.value = VISIBLE + val cs = ConstraintSet() + underTest.applyDefaultConstraints(cs) + val referencedIds = cs.getReferencedIds(R.id.weather_clock_date_and_icons_barrier_bottom) + referencedIds.contentEquals(intArrayOf(com.android.systemui.shared.R.id.bc_smartspace_view)) + } + + @Test + fun testSmartspaceGone_weatherClockDateAndIconsBarrierBottomBelowSmartspaceDateWeather() { + isAodIconsVisible.value = false + bcSmartspaceVisibility.value = GONE + val cs = ConstraintSet() + underTest.applyDefaultConstraints(cs) + val referencedIds = cs.getReferencedIds(R.id.weather_clock_date_and_icons_barrier_bottom) + referencedIds.contentEquals(intArrayOf(R.id.lockscreen_clock_view)) + } + + @Test + fun testHasAodIcons_weatherClockDateAndIconsBarrierBottomBelowSmartspaceDateWeather() { + isAodIconsVisible.value = true + val cs = ConstraintSet() + underTest.applyDefaultConstraints(cs) + val referencedIds = cs.getReferencedIds(R.id.weather_clock_date_and_icons_barrier_bottom) + referencedIds.contentEquals( + intArrayOf( + com.android.systemui.shared.R.id.bc_smartspace_view, + R.id.aod_notification_icon_container + ) + ) + } + private fun setLargeClock(useLargeClock: Boolean) { whenever(keyguardClockViewModel.useLargeClock).thenReturn(useLargeClock) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt index 1b4573dafe5e..22a2e93eb10d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt @@ -34,12 +34,14 @@ import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFaceConfig import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.shared.clocks.ClockRegistry +import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope @@ -68,6 +70,8 @@ class KeyguardClockViewModelTest : SysuiTestCase() { @Mock private lateinit var clockFaceConfig: ClockFaceConfig @Mock private lateinit var eventController: ClockEventController @Mock private lateinit var splitShadeStateController: SplitShadeStateController + @Mock private lateinit var notifsKeyguardInteractor: NotificationsKeyguardInteractor + @Mock private lateinit var areNotificationsFullyHidden: Flow<Boolean> @Before fun setup() { @@ -90,12 +94,15 @@ class KeyguardClockViewModelTest : SysuiTestCase() { scope.backgroundScope ) keyguardClockInteractor = KeyguardClockInteractor(keyguardClockRepository) + whenever(notifsKeyguardInteractor.areNotificationsFullyHidden) + .thenReturn(areNotificationsFullyHidden) underTest = KeyguardClockViewModel( keyguardInteractor, keyguardClockInteractor, scope.backgroundScope, splitShadeStateController, + notifsKeyguardInteractor ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 20d5c5de3ffa..49953a1176fd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor +import android.platform.test.annotations.EnableFlags import android.telephony.CellSignalStrength import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN @@ -159,10 +160,13 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun numberOfLevels_comesFromRepo() = + fun numberOfLevels_comesFromRepo_whenApplicable() = testScope.runTest { var latest: Int? = null - val job = underTest.signalLevelIcon.onEach { latest = it.numberOfLevels }.launchIn(this) + val job = + underTest.signalLevelIcon + .onEach { latest = (it as? SignalIconModel.Cellular)?.numberOfLevels } + .launchIn(this) connectionRepository.numberOfLevels.value = 5 assertThat(latest).isEqualTo(5) @@ -491,14 +495,19 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun iconId_correctLevel_notCutout() = + fun cellBasedIconId_correctLevel_notCutout() = testScope.runTest { + connectionRepository.isNonTerrestrial.value = false connectionRepository.isInService.value = true connectionRepository.primaryLevel.value = 1 connectionRepository.setDataEnabled(false) + connectionRepository.isNonTerrestrial.value = false - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + var latest: SignalIconModel.Cellular? = null + val job = + underTest.signalLevelIcon + .onEach { latest = it as? SignalIconModel.Cellular } + .launchIn(this) assertThat(latest?.level).isEqualTo(1) assertThat(latest?.showExclamationMark).isFalse() @@ -509,6 +518,7 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun icon_usesLevelFromInteractor() = testScope.runTest { + connectionRepository.isNonTerrestrial.value = false connectionRepository.isInService.value = true var latest: SignalIconModel? = null @@ -524,10 +534,15 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun icon_usesNumberOfLevelsFromInteractor() = + fun cellBasedIcon_usesNumberOfLevelsFromInteractor() = testScope.runTest { - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + connectionRepository.isNonTerrestrial.value = false + + var latest: SignalIconModel.Cellular? = null + val job = + underTest.signalLevelIcon + .onEach { latest = it as? SignalIconModel.Cellular } + .launchIn(this) connectionRepository.numberOfLevels.value = 5 assertThat(latest!!.numberOfLevels).isEqualTo(5) @@ -539,12 +554,16 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun icon_defaultDataDisabled_showExclamationTrue() = + fun cellBasedIcon_defaultDataDisabled_showExclamationTrue() = testScope.runTest { + connectionRepository.isNonTerrestrial.value = false mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = false - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + var latest: SignalIconModel.Cellular? = null + val job = + underTest.signalLevelIcon + .onEach { latest = it as? SignalIconModel.Cellular } + .launchIn(this) assertThat(latest!!.showExclamationMark).isTrue() @@ -552,12 +571,16 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun icon_defaultConnectionFailed_showExclamationTrue() = + fun cellBasedIcon_defaultConnectionFailed_showExclamationTrue() = testScope.runTest { + connectionRepository.isNonTerrestrial.value = false mobileIconsInteractor.isDefaultConnectionFailed.value = true - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + var latest: SignalIconModel.Cellular? = null + val job = + underTest.signalLevelIcon + .onEach { latest = it as? SignalIconModel.Cellular } + .launchIn(this) assertThat(latest!!.showExclamationMark).isTrue() @@ -565,14 +588,18 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun icon_enabledAndNotFailed_showExclamationFalse() = + fun cellBasedIcon_enabledAndNotFailed_showExclamationFalse() = testScope.runTest { + connectionRepository.isNonTerrestrial.value = false connectionRepository.isInService.value = true mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true mobileIconsInteractor.isDefaultConnectionFailed.value = false - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + var latest: SignalIconModel.Cellular? = null + val job = + underTest.signalLevelIcon + .onEach { latest = it as? SignalIconModel.Cellular } + .launchIn(this) assertThat(latest!!.showExclamationMark).isFalse() @@ -580,11 +607,15 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun icon_usesEmptyState_whenNotInService() = + fun cellBasedIcon_usesEmptyState_whenNotInService() = testScope.runTest { - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + var latest: SignalIconModel.Cellular? = null + val job = + underTest.signalLevelIcon + .onEach { latest = it as? SignalIconModel.Cellular } + .launchIn(this) + connectionRepository.isNonTerrestrial.value = false connectionRepository.isInService.value = false assertThat(latest?.level).isEqualTo(0) @@ -604,11 +635,15 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun icon_usesCarrierNetworkState_whenInCarrierNetworkChangeMode() = + fun cellBasedIcon_usesCarrierNetworkState_whenInCarrierNetworkChangeMode() = testScope.runTest { - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + var latest: SignalIconModel.Cellular? = null + val job = + underTest.signalLevelIcon + .onEach { latest = it as? SignalIconModel.Cellular? } + .launchIn(this) + connectionRepository.isNonTerrestrial.value = false connectionRepository.isInService.value = true connectionRepository.carrierNetworkChangeActive.value = true connectionRepository.primaryLevel.value = 1 @@ -626,6 +661,20 @@ class MobileIconInteractorTest : SysuiTestCase() { job.cancel() } + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @Test + fun satBasedIcon_isUsedWhenNonTerrestrial() = + testScope.runTest { + val latest by collectLastValue(underTest.signalLevelIcon) + + // Start off using cellular + assertThat(latest).isInstanceOf(SignalIconModel.Cellular::class.java) + + connectionRepository.isNonTerrestrial.value = true + + assertThat(latest).isInstanceOf(SignalIconModel.Satellite::class.java) + } + private fun createInteractor( overrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl() ) = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt index 90a894648c9f..ebec00380cf4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt @@ -32,7 +32,7 @@ internal class SignalIconModelParameterizedTest(private val testCase: TestCase) @Test fun drawableFromModel_level0_numLevels4_noExclamation_notCarrierNetworkChange() { val model = - SignalIconModel( + SignalIconModel.Cellular( level = 0, numberOfLevels = 4, showExclamationMark = false, @@ -59,7 +59,7 @@ internal class SignalIconModelParameterizedTest(private val testCase: TestCase) val expected: Int, ) { fun toSignalIconModel() = - SignalIconModel( + SignalIconModel.Cellular( level = level, numberOfLevels = numberOfLevels, showExclamationMark = showExclamation, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt index 889130f47820..deb9fcf4b56e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt @@ -190,7 +190,7 @@ class LocationBasedMobileIconViewModelTest : SysuiTestCase() { /** Convenience constructor for these tests */ fun defaultSignal(level: Int = 1): SignalIconModel { - return SignalIconModel( + return SignalIconModel.Cellular( level, NUM_LEVELS, showExclamationMark = false, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index 147efcbd67c4..83d0fe8f9c4b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -55,6 +55,7 @@ import com.android.systemui.util.CarrierConfigTracker import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope @@ -709,6 +710,87 @@ class MobileIconViewModelTest : SysuiTestCase() { .isEqualTo(Icon.Resource(R.drawable.mobile_network_type_background, null)) } + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @Test + fun nonTerrestrial_defaultProperties() = + testScope.runTest { + repository.isNonTerrestrial.value = true + + val roaming by collectLastValue(underTest.roaming) + val networkTypeIcon by collectLastValue(underTest.networkTypeIcon) + val networkTypeBackground by collectLastValue(underTest.networkTypeBackground) + val activityInVisible by collectLastValue(underTest.activityInVisible) + val activityOutVisible by collectLastValue(underTest.activityOutVisible) + val activityContainerVisible by collectLastValue(underTest.activityContainerVisible) + + assertThat(roaming).isFalse() + assertThat(networkTypeIcon).isNull() + assertThat(networkTypeBackground).isNull() + assertThat(activityInVisible).isFalse() + assertThat(activityOutVisible).isFalse() + assertThat(activityContainerVisible).isFalse() + } + + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @Test + fun nonTerrestrial_ignoresDefaultProperties() = + testScope.runTest { + repository.isNonTerrestrial.value = true + + val roaming by collectLastValue(underTest.roaming) + val networkTypeIcon by collectLastValue(underTest.networkTypeIcon) + val networkTypeBackground by collectLastValue(underTest.networkTypeBackground) + val activityInVisible by collectLastValue(underTest.activityInVisible) + val activityOutVisible by collectLastValue(underTest.activityOutVisible) + val activityContainerVisible by collectLastValue(underTest.activityContainerVisible) + + repository.setAllRoaming(true) + repository.setNetworkTypeKey(connectionsRepository.LTE_KEY) + // sets the background on cellular + repository.hasPrioritizedNetworkCapabilities.value = true + repository.dataActivityDirection.value = + DataActivityModel( + hasActivityIn = true, + hasActivityOut = true, + ) + + assertThat(roaming).isFalse() + assertThat(networkTypeIcon).isNull() + assertThat(networkTypeBackground).isNull() + assertThat(activityInVisible).isFalse() + assertThat(activityOutVisible).isFalse() + assertThat(activityContainerVisible).isFalse() + } + + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @Test + fun nonTerrestrial_usesSatelliteIcon() = + testScope.runTest { + repository.isNonTerrestrial.value = true + repository.setAllLevels(0) + + val latest by + collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) + + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + + // 1-2 -> 1 bar + repository.setAllLevels(1) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + repository.setAllLevels(2) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + // 3-4 -> 2 bars + repository.setAllLevels(3) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + + repository.setAllLevels(4) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + } + private fun createAndSetViewModel() { underTest = MobileIconViewModel( @@ -723,24 +805,5 @@ class MobileIconViewModelTest : SysuiTestCase() { companion object { private const val SUB_1_ID = 1 - private const val NUM_LEVELS = 4 - - /** Convenience constructor for these tests */ - fun defaultSignal(level: Int = 1): SignalIconModel { - return SignalIconModel( - level, - NUM_LEVELS, - showExclamationMark = false, - carrierNetworkChange = false, - ) - } - - fun emptySignal(): SignalIconModel = - SignalIconModel( - level = 0, - numberOfLevels = NUM_LEVELS, - showExclamationMark = true, - carrierNetworkChange = false, - ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt index 5ca0439c1313..4a85909ae996 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt @@ -20,6 +20,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor import com.android.systemui.statusbar.policy.splitShadeStateController val Kosmos.keyguardClockViewModel by @@ -29,5 +30,6 @@ val Kosmos.keyguardClockViewModel by keyguardClockInteractor = keyguardClockInteractor, applicationScope = applicationCoroutineScope, splitShadeStateController = splitShadeStateController, + notifsKeyguardInteractor = notificationsKeyguardInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index 5f4d7bf6f371..c01032757bb0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -60,6 +60,8 @@ class FakeMobileIconInteractor( override val isInService = MutableStateFlow(true) + override val isNonTerrestrial = MutableStateFlow(false) + private val _isDataEnabled = MutableStateFlow(true) override val isDataEnabled = _isDataEnabled @@ -69,7 +71,7 @@ class FakeMobileIconInteractor( override val signalLevelIcon: MutableStateFlow<SignalIconModel> = MutableStateFlow( - SignalIconModel( + SignalIconModel.Cellular( level = 0, numberOfLevels = 4, showExclamationMark = false, diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 9b1fade198fc..afb8345249b1 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -4491,6 +4491,12 @@ public final class ActiveServices { } } if (userId > 0) { + if (mAm.isSystemUserOnly(sInfo.flags)) { + Slog.w(TAG_SERVICE, service + " is only available for the SYSTEM user," + + " calling userId is: " + userId); + return null; + } + if (mAm.isSingleton(sInfo.processName, sInfo.applicationInfo, sInfo.name, sInfo.flags) && mAm.isValidSingletonCall(callingUid, sInfo.applicationInfo.uid)) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 86894fd9b405..3de224addf95 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -13763,6 +13763,11 @@ public class ActivityManagerService extends IActivityManager.Stub return result; } + boolean isSystemUserOnly(int flags) { + return android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders() + && (flags & ServiceInfo.FLAG_SYSTEM_USER_ONLY) != 0; + } + /** * Checks to see if the caller is in the same app as the singleton * component, or the component is in a special app. It allows special apps diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java index 095d907d7df6..30f21a65b5b1 100644 --- a/services/core/java/com/android/server/am/ContentProviderHelper.java +++ b/services/core/java/com/android/server/am/ContentProviderHelper.java @@ -1249,9 +1249,9 @@ public class ContentProviderHelper { ProviderInfo cpi = providers.get(i); boolean singleton = mService.isSingleton(cpi.processName, cpi.applicationInfo, cpi.name, cpi.flags); - if (singleton && app.userId != UserHandle.USER_SYSTEM) { - // This is a singleton provider, but a user besides the - // default user is asking to initialize a process it runs + if (isSingletonOrSystemUserOnly(cpi) && app.userId != UserHandle.USER_SYSTEM) { + // This is a singleton or a SYSTEM user only provider, but a user besides the + // SYSTEM user is asking to initialize a process it runs // in... well, no, it doesn't actually run in this process, // it runs in the process of the default user. Get rid of it. providers.remove(i); @@ -1398,8 +1398,7 @@ public class ContentProviderHelper { final boolean processMatch = Objects.equals(pi.processName, app.processName) || pi.multiprocess; - final boolean userMatch = !mService.isSingleton( - pi.processName, pi.applicationInfo, pi.name, pi.flags) + final boolean userMatch = !isSingletonOrSystemUserOnly(pi) || app.userId == UserHandle.USER_SYSTEM; final boolean isInstantApp = pi.applicationInfo.isInstantApp(); final boolean splitInstalled = pi.splitName == null @@ -1985,4 +1984,13 @@ public class ContentProviderHelper { return isAuthRedirected; } } + + /** + * Returns true if Provider is either singleUser or systemUserOnly provider. + */ + private boolean isSingletonOrSystemUserOnly(ProviderInfo pi) { + return (android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders() + && mService.isSystemUserOnly(pi.flags)) + || mService.isSingleton(pi.processName, pi.applicationInfo, pi.name, pi.flags); + } } diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 9db5d0a99480..dc14c7aaa0b9 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -162,6 +162,7 @@ public class SettingsToPropertiesMapper { "pdf_viewer", "pixel_audio_android", "pixel_bluetooth", + "pixel_connectivity_gps", "pixel_system_sw_video", "pixel_watch", "platform_security", diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index beb68d3566c3..50340d241347 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1167,7 +1167,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // sender userId can be a real user ID or USER_ALL. final int senderUserId = pendingResult.getSendingUserId(); if (senderUserId != UserHandle.USER_ALL) { - if (senderUserId != mSettings.getCurrentUserId()) { + if (senderUserId != mSettings.getUserId()) { // A background user is trying to hide the dialog. Ignore. return; } @@ -1245,7 +1245,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private boolean isChangingPackagesOfCurrentUserLocked() { final int userId = getChangingUserId(); - final boolean retval = userId == mSettings.getCurrentUserId(); + final boolean retval = userId == mSettings.getUserId(); if (DEBUG) { if (!retval) { Slog.d(TAG, "--- ignore this call back from a background user: " + userId); @@ -1344,8 +1344,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (changed) { AdditionalSubtypeUtils.save( - mAdditionalSubtypeMap, mSettings.getMethodMap(), - mSettings.getCurrentUserId()); + mAdditionalSubtypeMap, mSettings.getMethodMap(), mSettings.getUserId()); mChangedPackages.add(packageName); } } @@ -1425,8 +1424,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub + imi.getComponent()); mAdditionalSubtypeMap.remove(imi.getId()); AdditionalSubtypeUtils.save(mAdditionalSubtypeMap, - mSettings.getMethodMap(), - mSettings.getCurrentUserId()); + mSettings.getMethodMap(), mSettings.getUserId()); } } } @@ -1440,7 +1438,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (change == PACKAGE_TEMPORARY_CHANGE || change == PACKAGE_PERMANENT_CHANGE) { final PackageManager userAwarePackageManager = - getPackageManagerForUser(mContext, mSettings.getCurrentUserId()); + getPackageManagerForUser(mContext, mSettings.getUserId()); ServiceInfo si = null; try { si = userAwarePackageManager.getServiceInfo(curIm.getComponent(), @@ -1576,7 +1574,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub void onUnlockUser(@UserIdInt int userId) { synchronized (ImfLock.class) { - final int currentUserId = mSettings.getCurrentUserId(); + final int currentUserId = mSettings.getUserId(); if (DEBUG) { Slog.d(TAG, "onUnlockUser: userId=" + userId + " curUserId=" + currentUserId); } @@ -1665,7 +1663,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mSettings.getMethodMap(), userId); mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(mSettings.getMethodMap(), - mSettings.getCurrentUserId()); + mSettings.getUserId()); mMenuController = new InputMethodMenuController(this); mBindingController = bindingControllerForTesting != null @@ -1696,7 +1694,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") @UserIdInt int getCurrentImeUserIdLocked() { - return mSettings.getCurrentUserId(); + return mSettings.getUserId(); } private final class InkWindowInitializer implements Runnable { @@ -1784,7 +1782,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub IInputMethodClientInvoker clientToBeReset) { if (DEBUG) { Slog.d(TAG, "Switching user stage 1/3. newUserId=" + newUserId - + " currentUserId=" + mSettings.getCurrentUserId()); + + " currentUserId=" + mSettings.getUserId()); } maybeInitImeNavbarConfigLocked(newUserId); @@ -1854,7 +1852,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (!mSystemReady) { mSystemReady = true; - final int currentUserId = mSettings.getCurrentUserId(); + final int currentUserId = mSettings.getUserId(); mStatusBarManagerInternal = LocalServices.getService(StatusBarManagerInternal.class); hideStatusBarIconLocked(); @@ -1875,7 +1873,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // the "mImeDrawsImeNavBarResLazyInitFuture" field. synchronized (ImfLock.class) { mImeDrawsImeNavBarResLazyInitFuture = null; - if (currentUserId != mSettings.getCurrentUserId()) { + if (currentUserId != mSettings.getUserId()) { // This means that the current user is already switched to other user // before the background task is executed. In this scenario the relevant // field should already be initialized. @@ -1947,7 +1945,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } synchronized (ImfLock.class) { final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId, - mSettings.getCurrentUserId(), null); + mSettings.getUserId(), null); if (resolvedUserIds.length != 1) { return Collections.emptyList(); } @@ -1970,7 +1968,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } synchronized (ImfLock.class) { final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId, - mSettings.getCurrentUserId(), null); + mSettings.getUserId(), null); if (resolvedUserIds.length != 1) { return Collections.emptyList(); } @@ -1997,7 +1995,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } // Check if selected IME of current user supports handwriting. - if (userId == mSettings.getCurrentUserId()) { + if (userId == mSettings.getUserId()) { return mBindingController.supportsStylusHandwriting(); } //TODO(b/197848765): This can be optimized by caching multi-user methodMaps/methodList. @@ -2025,7 +2023,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private List<InputMethodInfo> getInputMethodListLocked(@UserIdInt int userId, @DirectBootAwareness int directBootAwareness, int callingUid) { final InputMethodSettings settings; - if (userId == mSettings.getCurrentUserId() + if (userId == mSettings.getUserId() && directBootAwareness == DirectBootAwareness.AUTO) { settings = mSettings; } else { @@ -2048,7 +2046,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub int callingUid) { final ArrayList<InputMethodInfo> methodList; final InputMethodSettings settings; - if (userId == mSettings.getCurrentUserId()) { + if (userId == mSettings.getUserId()) { methodList = mSettings.getEnabledInputMethodList(); settings = mSettings; } else { @@ -2112,7 +2110,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(String imiId, boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId, int callingUid) { - if (userId == mSettings.getCurrentUserId()) { + if (userId == mSettings.getUserId()) { final InputMethodInfo imi; String selectedMethodId = getSelectedMethodIdLocked(); if (imiId == null && selectedMethodId != null) { @@ -2300,7 +2298,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final boolean restarting = !initial; final Binder startInputToken = new Binder(); - final StartInputInfo info = new StartInputInfo(mSettings.getCurrentUserId(), + final StartInputInfo info = new StartInputInfo(mSettings.getUserId(), getCurTokenLocked(), mCurTokenDisplayId, getCurIdLocked(), startInputReason, restarting, UserHandle.getUserId(mCurClient.mUid), @@ -2315,9 +2313,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // same-user scenarios. // That said ignoring cross-user scenario will never affect IMEs that do not have // INTERACT_ACROSS_USERS(_FULL) permissions, which is actually almost always the case. - if (mSettings.getCurrentUserId() == UserHandle.getUserId( + if (mSettings.getUserId() == UserHandle.getUserId( mCurClient.mUid)) { - mPackageManagerInternal.grantImplicitAccess(mSettings.getCurrentUserId(), + mPackageManagerInternal.grantImplicitAccess(mSettings.getUserId(), null /* intent */, UserHandle.getAppId(getCurMethodUidLocked()), mCurClient.mUid, true /* direct */); } @@ -2939,7 +2937,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } else if (packageName != null) { if (DEBUG) Slog.d(TAG, "show a small icon for the input method"); final PackageManager userAwarePackageManager = - getPackageManagerForUser(mContext, mSettings.getCurrentUserId()); + getPackageManagerForUser(mContext, mSettings.getUserId()); ApplicationInfo applicationInfo = null; try { applicationInfo = userAwarePackageManager.getApplicationInfo(packageName, @@ -3001,7 +2999,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } if (mWindowManagerInternal.isKeyguardShowingAndNotOccluded() - && mWindowManagerInternal.isKeyguardSecure(mSettings.getCurrentUserId())) { + && mWindowManagerInternal.isKeyguardSecure(mSettings.getUserId())) { return false; } if ((visibility & InputMethodService.IME_ACTIVE) == 0 @@ -3180,7 +3178,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub void updateInputMethodsFromSettingsLocked(boolean enabledMayChange) { if (enabledMayChange) { final PackageManager userAwarePackageManager = getPackageManagerForUser(mContext, - mSettings.getCurrentUserId()); + mSettings.getUserId()); List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodList(); for (int i = 0; i < enabled.size(); i++) { @@ -3228,18 +3226,18 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } // TODO: Instantiate mSwitchingController for each user. - if (mSettings.getCurrentUserId() == mSwitchingController.getUserId()) { + if (mSettings.getUserId() == mSwitchingController.getUserId()) { mSwitchingController.resetCircularListLocked(mSettings.getMethodMap()); } else { mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked( - mContext, mSettings.getMethodMap(), mSettings.getCurrentUserId()); + mContext, mSettings.getMethodMap(), mSettings.getUserId()); } // TODO: Instantiate mHardwareKeyboardShortcutController for each user. - if (mSettings.getCurrentUserId() == mHardwareKeyboardShortcutController.getUserId()) { + if (mSettings.getUserId() == mHardwareKeyboardShortcutController.getUserId()) { mHardwareKeyboardShortcutController.reset(mSettings.getMethodMap()); } else { mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController( - mSettings.getMethodMap(), mSettings.getCurrentUserId()); + mSettings.getMethodMap(), mSettings.getUserId()); } sendOnNavButtonFlagsChangedLocked(); } @@ -3270,7 +3268,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // See if we need to notify a subtype change within the same IME. if (id.equals(getSelectedMethodIdLocked())) { - final int userId = mSettings.getCurrentUserId(); + final int userId = mSettings.getUserId(); final int subtypeCount = info.getSubtypeCount(); if (subtypeCount <= 0) { notifyInputMethodSubtypeChangedLocked(userId, info, null); @@ -3781,7 +3779,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return InputBindResult.USER_SWITCHING; } final int[] profileIdsWithDisabled = mUserManagerInternal.getProfileIds( - mSettings.getCurrentUserId(), false /* enabledOnly */); + mSettings.getUserId(), false /* enabledOnly */); for (int profileId : profileIdsWithDisabled) { if (profileId == userId) { scheduleSwitchUserTaskLocked(userId, cs.mClient); @@ -3800,7 +3798,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mVisibilityStateComputer.mShowForced = false; } - final int currentUserId = mSettings.getCurrentUserId(); + final int currentUserId = mSettings.getUserId(); if (userId != currentUserId) { if (ArrayUtils.contains( mUserManagerInternal.getProfileIds(currentUserId, false), userId)) { @@ -3944,7 +3942,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub && mCurFocusedWindowClient.mClient.asBinder() == client.asBinder()) { return true; } - if (mSettings.getCurrentUserId() != UserHandle.getUserId(uid)) { + if (mSettings.getUserId() != UserHandle.getUserId(uid)) { return false; } if (getCurIntentLocked() != null && InputMethodUtils.checkIfPackageBelongsToUid( @@ -4085,8 +4083,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub && !TextUtils.isEmpty(mCurrentSubtype.getLocale())) { locale = mCurrentSubtype.getLocale(); } else { - locale = SystemLocaleWrapper.get(mSettings.getCurrentUserId()).get(0) - .toString(); + locale = SystemLocaleWrapper.get(mSettings.getUserId()).get(0).toString(); } for (int i = 0; i < enabledCount; ++i) { final InputMethodInfo imi = enabled.get(i); @@ -4164,7 +4161,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); } synchronized (ImfLock.class) { - if (mSettings.getCurrentUserId() == userId) { + if (mSettings.getUserId() == userId) { return mSettings.getLastInputMethodSubtype(); } @@ -4199,7 +4196,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return; } - if (mSettings.getCurrentUserId() == userId) { + if (mSettings.getUserId() == userId) { if (!mSettings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, mAdditionalSubtypeMap, mPackageManagerInternal, callingUid)) { return; @@ -4243,7 +4240,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final long ident = Binder.clearCallingIdentity(); try { synchronized (ImfLock.class) { - final boolean currentUser = (mSettings.getCurrentUserId() == userId); + final boolean currentUser = (mSettings.getUserId() == userId); final InputMethodSettings settings = currentUser ? mSettings : queryMethodMapForUser(userId); if (!settings.setEnabledInputMethodSubtypes(imeId, subtypeHashCodes)) { @@ -4614,7 +4611,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } return; } - if (mSettings.getCurrentUserId() != mSwitchingController.getUserId()) { + if (mSettings.getUserId() != mSwitchingController.getUserId()) { return; } final InputMethodInfo imi = @@ -4849,8 +4846,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } synchronized (ImfLock.class) { final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked() - && mWindowManagerInternal.isKeyguardSecure( - mSettings.getCurrentUserId()); + && mWindowManagerInternal.isKeyguardSecure(mSettings.getUserId()); final String lastInputMethodId = mSettings.getSelectedInputMethod(); int lastInputMethodSubtypeId = mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId); @@ -4858,8 +4854,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController .getSortedInputMethodAndSubtypeList( showAuxSubtypes, isScreenLocked, true /* forImeMenu */, - mContext, mSettings.getMethodMap(), - mSettings.getCurrentUserId()); + mContext, mSettings.getMethodMap(), mSettings.getUserId()); mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId, lastInputMethodId, lastInputMethodSubtypeId, imList); } @@ -5157,7 +5152,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mMethodMapUpdateCount++; mMyPackageMonitor.clearKnownImePackageNamesLocked(); - mSettings = queryInputMethodServicesInternal(mContext, mSettings.getCurrentUserId(), + mSettings = queryInputMethodServicesInternal(mContext, mSettings.getUserId(), mAdditionalSubtypeMap, DirectBootAwareness.AUTO); // Construct the set of possible IME packages for onPackageChanged() to avoid false @@ -5170,7 +5165,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final List<ResolveInfo> allInputMethodServices = mContext.getPackageManager().queryIntentServicesAsUser( new Intent(InputMethod.SERVICE_INTERFACE), - PackageManager.MATCH_DISABLED_COMPONENTS, mSettings.getCurrentUserId()); + PackageManager.MATCH_DISABLED_COMPONENTS, mSettings.getUserId()); final int numImes = allInputMethodServices.size(); for (int i = 0; i < numImes; ++i) { final ServiceInfo si = allInputMethodServices.get(i).serviceInfo; @@ -5241,18 +5236,18 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub updateDefaultVoiceImeIfNeededLocked(); // TODO: Instantiate mSwitchingController for each user. - if (mSettings.getCurrentUserId() == mSwitchingController.getUserId()) { + if (mSettings.getUserId() == mSwitchingController.getUserId()) { mSwitchingController.resetCircularListLocked(mSettings.getMethodMap()); } else { mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked( - mContext, mSettings.getMethodMap(), mSettings.getCurrentUserId()); + mContext, mSettings.getMethodMap(), mSettings.getUserId()); } // TODO: Instantiate mHardwareKeyboardShortcutController for each user. - if (mSettings.getCurrentUserId() == mHardwareKeyboardShortcutController.getUserId()) { + if (mSettings.getUserId() == mHardwareKeyboardShortcutController.getUserId()) { mHardwareKeyboardShortcutController.reset(mSettings.getMethodMap()); } else { mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController( - mSettings.getMethodMap(), mSettings.getCurrentUserId()); + mSettings.getMethodMap(), mSettings.getUserId()); } sendOnNavButtonFlagsChangedLocked(); @@ -5260,7 +5255,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // Notify InputMethodListListeners of the new installed InputMethods. final List<InputMethodInfo> inputMethodList = mSettings.getMethodList(); mHandler.obtainMessage(MSG_DISPATCH_ON_INPUT_METHOD_LIST_UPDATED, - mSettings.getCurrentUserId(), 0 /* unused */, inputMethodList).sendToTarget(); + mSettings.getUserId(), 0 /* unused */, inputMethodList).sendToTarget(); } @GuardedBy("ImfLock.class") @@ -5381,7 +5376,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mCurrentSubtype = getCurrentInputMethodSubtypeLocked(); } } - notifyInputMethodSubtypeChangedLocked(mSettings.getCurrentUserId(), imi, mCurrentSubtype); + notifyInputMethodSubtypeChangedLocked(mSettings.getUserId(), imi, mCurrentSubtype); if (!setSubtypeOnly) { // Set InputMethod here @@ -5422,7 +5417,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); } synchronized (ImfLock.class) { - if (mSettings.getCurrentUserId() == userId) { + if (mSettings.getUserId() == userId) { return getCurrentInputMethodSubtypeLocked(); } @@ -5466,7 +5461,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) { mCurrentSubtype = explicitlyOrImplicitlyEnabledSubtypes.get(0); } else if (explicitlyOrImplicitlyEnabledSubtypes.size() > 1) { - final String locale = SystemLocaleWrapper.get(mSettings.getCurrentUserId()) + final String locale = SystemLocaleWrapper.get(mSettings.getUserId()) .get(0).toString(); mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, @@ -5490,7 +5485,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private InputMethodInfo queryDefaultInputMethodForUserIdLocked(@UserIdInt int userId) { final InputMethodSettings settings; - if (userId == mSettings.getCurrentUserId()) { + if (userId == mSettings.getUserId()) { settings = mSettings; } else { final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = @@ -5512,7 +5507,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private boolean switchToInputMethodLocked(String imeId, @UserIdInt int userId) { - if (userId == mSettings.getCurrentUserId()) { + if (userId == mSettings.getUserId()) { if (!mSettings.getMethodMap().containsKey(imeId) || !mSettings.getEnabledInputMethodList() .contains(mSettings.getMethodMap().get(imeId))) { @@ -5654,7 +5649,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public boolean setInputMethodEnabled(String imeId, boolean enabled, @UserIdInt int userId) { synchronized (ImfLock.class) { - if (userId == mSettings.getCurrentUserId()) { + if (userId == mSettings.getUserId()) { if (!mSettings.getMethodMap().containsKey(imeId)) { return false; // IME is not found. } @@ -6296,7 +6291,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } synchronized (ImfLock.class) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter()); + mSettings.getUserId(), shellCommand.getErrPrintWriter()); try (PrintWriter pr = shellCommand.getOutPrintWriter()) { for (int userId : userIds) { final List<InputMethodInfo> methods = all @@ -6341,7 +6336,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub PrintWriter error = shellCommand.getErrPrintWriter()) { synchronized (ImfLock.class) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter()); + mSettings.getUserId(), shellCommand.getErrPrintWriter()); for (int userId : userIds) { if (!userHasDebugPriv(userId, shellCommand)) { continue; @@ -6400,7 +6395,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub PrintWriter error) { boolean failedToEnableUnknownIme = false; boolean previouslyEnabled = false; - if (userId == mSettings.getCurrentUserId()) { + if (userId == mSettings.getUserId()) { if (enabled && !mSettings.getMethodMap().containsKey(imeId)) { failedToEnableUnknownIme = true; } else { @@ -6462,7 +6457,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub PrintWriter error = shellCommand.getErrPrintWriter()) { synchronized (ImfLock.class) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter()); + mSettings.getUserId(), shellCommand.getErrPrintWriter()); for (int userId : userIds) { if (!userHasDebugPriv(userId, shellCommand)) { continue; @@ -6502,7 +6497,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub synchronized (ImfLock.class) { try (PrintWriter out = shellCommand.getOutPrintWriter()) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter()); + mSettings.getUserId(), shellCommand.getErrPrintWriter()); for (int userId : userIds) { if (!userHasDebugPriv(userId, shellCommand)) { continue; @@ -6514,7 +6509,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } final String nextIme; final List<InputMethodInfo> nextEnabledImes; - if (userId == mSettings.getCurrentUserId()) { + if (userId == mSettings.getUserId()) { hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */, null /* resultReceiver */, SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND); @@ -6537,7 +6532,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } updateInputMethodsFromSettingsLocked(true /* enabledMayChange */); InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( - getPackageManagerForUser(mContext, mSettings.getCurrentUserId()), + getPackageManagerForUser(mContext, mSettings.getUserId()), mSettings.getEnabledInputMethodList()); nextIme = mSettings.getSelectedInputMethod(); nextEnabledImes = mSettings.getEnabledInputMethodList(); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java index abd7688b60cc..a51002be344f 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java @@ -63,7 +63,7 @@ final class InputMethodSettings { private final List<InputMethodInfo> mMethodList; @UserIdInt - private final int mCurrentUserId; + private final int mUserId; private static void buildEnabledInputMethodsSettingString( StringBuilder builder, Pair<String, ArrayList<String>> ime) { @@ -87,7 +87,7 @@ final class InputMethodSettings { private InputMethodSettings(InputMethodMap methodMap, @UserIdInt int userId) { mMethodMap = methodMap; mMethodList = methodMap.values(); - mCurrentUserId = userId; + mUserId = userId; String ime = getSelectedInputMethod(); String defaultDeviceIme = getSelectedDefaultDeviceInputMethod(); if (defaultDeviceIme != null && !Objects.equals(ime, defaultDeviceIme)) { @@ -109,20 +109,20 @@ final class InputMethodSettings { } private void putString(@NonNull String key, @Nullable String str) { - SecureSettingsWrapper.putString(key, str, mCurrentUserId); + SecureSettingsWrapper.putString(key, str, mUserId); } @Nullable private String getString(@NonNull String key, @Nullable String defaultValue) { - return SecureSettingsWrapper.getString(key, defaultValue, mCurrentUserId); + return SecureSettingsWrapper.getString(key, defaultValue, mUserId); } private void putInt(String key, int value) { - SecureSettingsWrapper.putInt(key, value, mCurrentUserId); + SecureSettingsWrapper.putInt(key, value, mUserId); } private int getInt(String key, int defaultValue) { - return SecureSettingsWrapper.getInt(key, defaultValue, mCurrentUserId); + return SecureSettingsWrapper.getInt(key, defaultValue, mUserId); } ArrayList<InputMethodInfo> getEnabledInputMethodList() { @@ -142,7 +142,7 @@ final class InputMethodSettings { getEnabledInputMethodSubtypeList(imi); if (allowsImplicitlyEnabledSubtypes && enabledSubtypes.isEmpty()) { enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypes( - SystemLocaleWrapper.get(mCurrentUserId), imi); + SystemLocaleWrapper.get(mUserId), imi); } return InputMethodSubtype.sort(imi, enabledSubtypes); } @@ -394,7 +394,7 @@ final class InputMethodSettings { private String getEnabledSubtypeHashCodeForInputMethodAndSubtype(List<Pair<String, ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) { - final LocaleList localeList = SystemLocaleWrapper.get(mCurrentUserId); + final LocaleList localeList = SystemLocaleWrapper.get(mUserId); for (int i = 0; i < enabledImes.size(); ++i) { final Pair<String, ArrayList<String>> enabledIme = enabledImes.get(i); if (enabledIme.first.equals(imeId)) { @@ -483,16 +483,14 @@ final class InputMethodSettings { void putSelectedInputMethod(String imeId) { if (DEBUG) { - Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", " - + mCurrentUserId); + Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", " + mUserId); } putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId); } void putSelectedSubtype(int subtypeId) { if (DEBUG) { - Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", " - + mCurrentUserId); + Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", " + mUserId); } putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId); } @@ -510,24 +508,21 @@ final class InputMethodSettings { String getSelectedDefaultDeviceInputMethod() { final String imi = getString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null); if (DEBUG) { - Slog.d(TAG, "getSelectedDefaultDeviceInputMethodStr: " + imi + ", " - + mCurrentUserId); + Slog.d(TAG, "getSelectedDefaultDeviceInputMethodStr: " + imi + ", " + mUserId); } return imi; } void putSelectedDefaultDeviceInputMethod(String imeId) { if (DEBUG) { - Slog.d(TAG, "putSelectedDefaultDeviceInputMethodStr: " + imeId + ", " - + mCurrentUserId); + Slog.d(TAG, "putSelectedDefaultDeviceInputMethodStr: " + imeId + ", " + mUserId); } putString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, imeId); } void putDefaultVoiceInputMethod(String imeId) { if (DEBUG) { - Slog.d(TAG, - "putDefaultVoiceInputMethodStr: " + imeId + ", " + mCurrentUserId); + Slog.d(TAG, "putDefaultVoiceInputMethodStr: " + imeId + ", " + mUserId); } putString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, imeId); } @@ -551,8 +546,8 @@ final class InputMethodSettings { } @UserIdInt - public int getCurrentUserId() { - return mCurrentUserId; + public int getUserId() { + return mUserId; } int getSelectedInputMethodSubtypeId(String selectedImiId) { @@ -615,7 +610,7 @@ final class InputMethodSettings { if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) { return explicitlyOrImplicitlyEnabledSubtypes.get(0); } - final String locale = SystemLocaleWrapper.get(mCurrentUserId).get(0).toString(); + final String locale = SystemLocaleWrapper.get(mUserId).get(0).toString(); final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); @@ -644,7 +639,7 @@ final class InputMethodSettings { } else { additionalSubtypeMap.put(imi.getId(), subtypes); } - AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getCurrentUserId()); + AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getUserId()); return true; } @@ -715,6 +710,6 @@ final class InputMethodSettings { } void dump(final Printer pw, final String prefix) { - pw.println(prefix + "mCurrentUserId=" + mCurrentUserId); + pw.println(prefix + "mUserId=" + mUserId); } } diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 45dd02c9b7be..f3f183815d0b 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -56,6 +56,7 @@ #include "java/JavaClassGenerator.h" #include "java/ManifestClassGenerator.h" #include "java/ProguardRules.h" +#include "link/FeatureFlagsFilter.h" #include "link/Linkers.h" #include "link/ManifestFixer.h" #include "link/NoDefaultResourceRemover.h" @@ -1987,6 +1988,19 @@ class Linker { context_->SetNameManglerPolicy(NameManglerPolicy{context_->GetCompilationPackage()}); context_->SetSplitNameDependencies(app_info_.split_name_dependencies); + FeatureFlagsFilterOptions flags_filter_options; + if (context_->GetMinSdkVersion() > SDK_UPSIDE_DOWN_CAKE) { + // For API version > U, PackageManager will dynamically read the flag values and disable + // manifest elements accordingly when parsing the manifest. + // For API version <= U, we remove disabled elements from the manifest with the filter. + flags_filter_options.remove_disabled_elements = false; + flags_filter_options.flags_must_have_value = false; + } + FeatureFlagsFilter flags_filter(options_.feature_flag_values, flags_filter_options); + if (!flags_filter.Consume(context_, manifest_xml.get())) { + return 1; + } + // Override the package ID when it is "android". if (context_->GetCompilationPackage() == "android") { context_->SetPackageId(kAndroidPackageId); @@ -2531,7 +2545,7 @@ int LinkCommand::Action(const std::vector<std::string>& args) { } for (const std::string& arg : all_feature_flags_args) { - if (ParseFeatureFlagsParameter(arg, context.GetDiagnostics(), &options_.feature_flag_values)) { + if (!ParseFeatureFlagsParameter(arg, context.GetDiagnostics(), &options_.feature_flag_values)) { return 1; } } diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index 26713fd92264..dc18b1ccda60 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -330,7 +330,11 @@ class LinkCommand : public Command { "should only be used together with the --static-lib flag.", &options_.merge_only); AddOptionalSwitch("-v", "Enables verbose logging.", &verbose_); - AddOptionalFlagList("--feature-flags", "Placeholder, to be implemented.", &feature_flags_args_); + AddOptionalFlagList("--feature-flags", + "Specify the values of feature flags. The pairs in the argument\n" + "are separated by ',' and the name is separated from the value by '='.\n" + "Example: \"flag1=true,flag2=false,flag3=\" (flag3 has no given value).", + &feature_flags_args_); } int Action(const std::vector<std::string>& args) override; diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp index 7096f5cc54e3..9323f3b95eac 100644 --- a/tools/aapt2/cmd/Link_test.cpp +++ b/tools/aapt2/cmd/Link_test.cpp @@ -16,11 +16,10 @@ #include "Link.h" -#include <android-base/file.h> - -#include "AppInfo.h" #include "Diagnostics.h" #include "LoadedApk.h" +#include "android-base/file.h" +#include "android-base/stringprintf.h" #include "test/Test.h" using testing::Eq; @@ -993,4 +992,213 @@ TEST_F(LinkTest, LocaleConfigWrongLocaleFormat) { ASSERT_FALSE(Link(link_args, &diag)); } +static void BuildSDKWithFeatureFlagAttr(const std::string& apk_path, const std::string& java_path, + CommandTestFixture* fixture, android::IDiagnostics* diag) { + const std::string android_values = + R"(<resources> + <staging-public-group type="attr" first-id="0x01fe0063"> + <public name="featureFlag" /> + </staging-public-group> + <attr name="featureFlag" format="string" /> + </resources>)"; + + SourceXML source_xml{.res_file_path = "/res/values/values.xml", .file_contents = android_values}; + BuildSDK({source_xml}, apk_path, java_path, fixture, diag); +} + +TEST_F(LinkTest, FeatureFlagDisabled_SdkAtMostUDC) { + StdErrDiagnostics diag; + const std::string android_apk = GetTestPath("android.apk"); + const std::string android_java = GetTestPath("android-java"); + BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag); + + const std::string manifest_contents = android::base::StringPrintf( + R"(<uses-sdk android:minSdkVersion="%d" />" + <permission android:name="FOO" android:featureFlag="flag" />)", + SDK_UPSIDE_DOWN_CAKE); + auto app_manifest = ManifestBuilder(this) + .SetPackageName("com.example.app") + .AddContents(manifest_contents) + .Build(); + + auto app_link_args = LinkCommandBuilder(this) + .SetManifestFile(app_manifest) + .AddParameter("-I", android_apk) + .AddParameter("--feature-flags", "flag=false"); + + const std::string app_apk = GetTestPath("app.apk"); + BuildApk({}, app_apk, std::move(app_link_args), this, &diag); + + // Permission element should be removed if flag is disabled + auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag); + ASSERT_THAT(apk, NotNull()); + auto apk_manifest = apk->GetManifest(); + ASSERT_THAT(apk_manifest, NotNull()); + auto root = apk_manifest->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, IsNull()); +} + +TEST_F(LinkTest, FeatureFlagEnabled_SdkAtMostUDC) { + StdErrDiagnostics diag; + const std::string android_apk = GetTestPath("android.apk"); + const std::string android_java = GetTestPath("android-java"); + BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag); + + const std::string manifest_contents = android::base::StringPrintf( + R"(<uses-sdk android:minSdkVersion="%d" />" + <permission android:name="FOO" android:featureFlag="flag" />)", + SDK_UPSIDE_DOWN_CAKE); + auto app_manifest = ManifestBuilder(this) + .SetPackageName("com.example.app") + .AddContents(manifest_contents) + .Build(); + + auto app_link_args = LinkCommandBuilder(this) + .SetManifestFile(app_manifest) + .AddParameter("-I", android_apk) + .AddParameter("--feature-flags", "flag=true"); + + const std::string app_apk = GetTestPath("app.apk"); + BuildApk({}, app_apk, std::move(app_link_args), this, &diag); + + // Permission element should be kept if flag is enabled + auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag); + ASSERT_THAT(apk, NotNull()); + auto apk_manifest = apk->GetManifest(); + ASSERT_THAT(apk_manifest, NotNull()); + auto root = apk_manifest->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, NotNull()); +} + +TEST_F(LinkTest, FeatureFlagWithNoValue_SdkAtMostUDC) { + StdErrDiagnostics diag; + const std::string android_apk = GetTestPath("android.apk"); + const std::string android_java = GetTestPath("android-java"); + BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag); + + const std::string manifest_contents = android::base::StringPrintf( + R"(<uses-sdk android:minSdkVersion="%d" />" + <permission android:name="FOO" android:featureFlag="flag" />)", + SDK_UPSIDE_DOWN_CAKE); + auto app_manifest = ManifestBuilder(this) + .SetPackageName("com.example.app") + .AddContents(manifest_contents) + .Build(); + + auto app_link_args = LinkCommandBuilder(this) + .SetManifestFile(app_manifest) + .AddParameter("-I", android_apk) + .AddParameter("--feature-flags", "flag="); + + // Flags must have values if <= UDC + const std::string app_apk = GetTestPath("app.apk"); + ASSERT_FALSE(Link(app_link_args.Build(app_apk), &diag)); +} + +TEST_F(LinkTest, FeatureFlagDisabled_SdkAfterUDC) { + StdErrDiagnostics diag; + const std::string android_apk = GetTestPath("android.apk"); + const std::string android_java = GetTestPath("android-java"); + BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag); + + const std::string manifest_contents = android::base::StringPrintf( + R"(<uses-sdk android:minSdkVersion="%d" />" + <permission android:name="FOO" android:featureFlag="flag" />)", + SDK_CUR_DEVELOPMENT); + auto app_manifest = ManifestBuilder(this) + .SetPackageName("com.example.app") + .AddContents(manifest_contents) + .Build(); + + auto app_link_args = LinkCommandBuilder(this) + .SetManifestFile(app_manifest) + .AddParameter("-I", android_apk) + .AddParameter("--feature-flags", "flag=false"); + + const std::string app_apk = GetTestPath("app.apk"); + BuildApk({}, app_apk, std::move(app_link_args), this, &diag); + + // Permission element should be kept if > UDC, regardless of flag value + auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag); + ASSERT_THAT(apk, NotNull()); + auto apk_manifest = apk->GetManifest(); + ASSERT_THAT(apk_manifest, NotNull()); + auto root = apk_manifest->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, NotNull()); +} + +TEST_F(LinkTest, FeatureFlagEnabled_SdkAfterUDC) { + StdErrDiagnostics diag; + const std::string android_apk = GetTestPath("android.apk"); + const std::string android_java = GetTestPath("android-java"); + BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag); + + const std::string manifest_contents = android::base::StringPrintf( + R"(<uses-sdk android:minSdkVersion="%d" />" + <permission android:name="FOO" android:featureFlag="flag" />)", + SDK_CUR_DEVELOPMENT); + auto app_manifest = ManifestBuilder(this) + .SetPackageName("com.example.app") + .AddContents(manifest_contents) + .Build(); + + auto app_link_args = LinkCommandBuilder(this) + .SetManifestFile(app_manifest) + .AddParameter("-I", android_apk) + .AddParameter("--feature-flags", "flag=true"); + + const std::string app_apk = GetTestPath("app.apk"); + BuildApk({}, app_apk, std::move(app_link_args), this, &diag); + + // Permission element should be kept if > UDC, regardless of flag value + auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag); + ASSERT_THAT(apk, NotNull()); + auto apk_manifest = apk->GetManifest(); + ASSERT_THAT(apk_manifest, NotNull()); + auto root = apk_manifest->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, NotNull()); +} + +TEST_F(LinkTest, FeatureFlagWithNoValue_SdkAfterUDC) { + StdErrDiagnostics diag; + const std::string android_apk = GetTestPath("android.apk"); + const std::string android_java = GetTestPath("android-java"); + BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag); + + const std::string manifest_contents = android::base::StringPrintf( + R"(<uses-sdk android:minSdkVersion="%d" />" + <permission android:name="FOO" android:featureFlag="flag" />)", + SDK_CUR_DEVELOPMENT); + auto app_manifest = ManifestBuilder(this) + .SetPackageName("com.example.app") + .AddContents(manifest_contents) + .Build(); + + auto app_link_args = LinkCommandBuilder(this) + .SetManifestFile(app_manifest) + .AddParameter("-I", android_apk) + .AddParameter("--feature-flags", "flag="); + + const std::string app_apk = GetTestPath("app.apk"); + BuildApk({}, app_apk, std::move(app_link_args), this, &diag); + + // Permission element should be kept if > UDC, regardless of flag value + auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag); + ASSERT_THAT(apk, NotNull()); + auto apk_manifest = apk->GetManifest(); + ASSERT_THAT(apk_manifest, NotNull()); + auto root = apk_manifest->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, NotNull()); +} + } // namespace aapt diff --git a/tools/aapt2/java/AnnotationProcessor.cpp b/tools/aapt2/java/AnnotationProcessor.cpp index 8c644cf83339..a7f6f5524e21 100644 --- a/tools/aapt2/java/AnnotationProcessor.cpp +++ b/tools/aapt2/java/AnnotationProcessor.cpp @@ -64,29 +64,31 @@ static std::array<AnnotationRule, 3> sAnnotationRules = {{ {"@FlaggedApi", AnnotationRule::kFlaggedApi, "@android.annotation.FlaggedApi", true}, }}; -void AnnotationProcessor::AppendCommentLine(std::string comment) { +void AnnotationProcessor::AppendCommentLine(std::string comment, bool add_api_annotations) { static constexpr std::string_view sDeprecated = "@deprecated"; - // Treat deprecated specially, since we don't remove it from the source comment. - if (comment.find(sDeprecated) != std::string::npos) { - annotation_parameter_map_[AnnotationRule::kDeprecated] = ""; - } + if (add_api_annotations) { + // Treat deprecated specially, since we don't remove it from the source comment. + if (comment.find(sDeprecated) != std::string::npos) { + annotation_parameter_map_[AnnotationRule::kDeprecated] = ""; + } - for (const AnnotationRule& rule : sAnnotationRules) { - std::string::size_type idx = comment.find(rule.doc_str.data()); - if (idx != std::string::npos) { - // Captures all parameters associated with the specified annotation rule - // by matching the first pair of parentheses after the rule. - std::regex re(std::string(rule.doc_str).append(R"(\s*\((.+)\))")); - std::smatch match_result; - const bool is_match = std::regex_search(comment, match_result, re); - if (is_match && rule.preserve_params) { - annotation_parameter_map_[rule.bit_mask] = match_result[1].str(); - comment.erase(comment.begin() + match_result.position(), - comment.begin() + match_result.position() + match_result.length()); - } else { - annotation_parameter_map_[rule.bit_mask] = ""; - comment.erase(comment.begin() + idx, comment.begin() + idx + rule.doc_str.size()); + for (const AnnotationRule& rule : sAnnotationRules) { + std::string::size_type idx = comment.find(rule.doc_str.data()); + if (idx != std::string::npos) { + // Captures all parameters associated with the specified annotation rule + // by matching the first pair of parentheses after the rule. + std::regex re(std::string(rule.doc_str).append(R"(\s*\((.+)\))")); + std::smatch match_result; + const bool is_match = std::regex_search(comment, match_result, re); + if (is_match && rule.preserve_params) { + annotation_parameter_map_[rule.bit_mask] = match_result[1].str(); + comment.erase(comment.begin() + match_result.position(), + comment.begin() + match_result.position() + match_result.length()); + } else { + annotation_parameter_map_[rule.bit_mask] = ""; + comment.erase(comment.begin() + idx, comment.begin() + idx + rule.doc_str.size()); + } } } } @@ -109,12 +111,12 @@ void AnnotationProcessor::AppendCommentLine(std::string comment) { comment_ << "\n * " << std::move(comment); } -void AnnotationProcessor::AppendComment(StringPiece comment) { +void AnnotationProcessor::AppendComment(StringPiece comment, bool add_api_annotations) { // We need to process line by line to clean-up whitespace and append prefixes. for (StringPiece line : util::Tokenize(comment, '\n')) { line = util::TrimWhitespace(line); if (!line.empty()) { - AppendCommentLine(std::string(line)); + AppendCommentLine(std::string(line), add_api_annotations); } } } diff --git a/tools/aapt2/java/AnnotationProcessor.h b/tools/aapt2/java/AnnotationProcessor.h index db3437e3b5b1..2217ab35705a 100644 --- a/tools/aapt2/java/AnnotationProcessor.h +++ b/tools/aapt2/java/AnnotationProcessor.h @@ -60,7 +60,10 @@ class AnnotationProcessor { // Adds more comments. Resources can have value definitions for various configurations, and // each of the definitions may have comments that need to be processed. - void AppendComment(android::StringPiece comment); + // + // If add_api_annotations is false, annotations found in the comment (e.g., "@SystemApi") + // will NOT be converted to Java annotations. + void AppendComment(android::StringPiece comment, bool add_api_annotations = true); void AppendNewLine(); @@ -73,7 +76,7 @@ class AnnotationProcessor { bool has_comments_ = false; std::unordered_map<uint32_t, std::string> annotation_parameter_map_; - void AppendCommentLine(std::string line); + void AppendCommentLine(std::string line, bool add_api_annotations); }; } // namespace aapt diff --git a/tools/aapt2/java/AnnotationProcessor_test.cpp b/tools/aapt2/java/AnnotationProcessor_test.cpp index e98e96ba3bc3..e5eee34f451c 100644 --- a/tools/aapt2/java/AnnotationProcessor_test.cpp +++ b/tools/aapt2/java/AnnotationProcessor_test.cpp @@ -136,7 +136,28 @@ TEST(AnnotationProcessorTest, NotEmitSystemApiAnnotation) { EXPECT_THAT(annotations, HasSubstr("This is a system API")); } -TEST(AnnotationProcessor, ExtractsFirstSentence) { +TEST(AnnotationProcessorTest, DoNotAddApiAnnotations) { + AnnotationProcessor processor; + processor.AppendComment( + "@SystemApi This is a system API\n" + "@FlaggedApi This is a flagged API\n" + "@TestApi This is a test API\n" + "@deprecated Deprecate me\n", /*add_api_annotations=*/ + false); + + std::string annotations; + StringOutputStream out(&annotations); + Printer printer(&out); + processor.Print(&printer); + out.Flush(); + + EXPECT_THAT(annotations, Not(HasSubstr("@android.annotation.SystemApi"))); + EXPECT_THAT(annotations, Not(HasSubstr("@android.annotation.FlaggedApi"))); + EXPECT_THAT(annotations, Not(HasSubstr("@android.annotation.TestApi"))); + EXPECT_THAT(annotations, Not(HasSubstr("@Deprecated"))); +} + +TEST(AnnotationProcessorTest, ExtractsFirstSentence) { EXPECT_THAT(AnnotationProcessor::ExtractFirstSentence("This is the only sentence"), Eq("This is the only sentence")); EXPECT_THAT(AnnotationProcessor::ExtractFirstSentence( diff --git a/tools/aapt2/java/JavaClassGenerator.cpp b/tools/aapt2/java/JavaClassGenerator.cpp index 58f656458177..6e73b017cce2 100644 --- a/tools/aapt2/java/JavaClassGenerator.cpp +++ b/tools/aapt2/java/JavaClassGenerator.cpp @@ -180,7 +180,10 @@ static void AddAttributeFormatDoc(AnnotationProcessor* processor, Attribute* att << "<td>" << std::hex << symbol.value << std::dec << "</td>" << "<td>" << util::TrimWhitespace(symbol.symbol.GetComment()) << "</td></tr>"; - processor->AppendComment(line.str()); + // add_api_annotations is false since we don't want any annotations + // (e.g., "@deprecated")/ found in the enum/flag values to be propagated + // up to the attribute. + processor->AppendComment(line.str(), /*add_api_annotations=*/false); } processor->AppendComment("</table>"); } diff --git a/tools/aapt2/java/JavaClassGenerator_test.cpp b/tools/aapt2/java/JavaClassGenerator_test.cpp index 40395ed64fe3..bca9f4bc58c4 100644 --- a/tools/aapt2/java/JavaClassGenerator_test.cpp +++ b/tools/aapt2/java/JavaClassGenerator_test.cpp @@ -324,7 +324,58 @@ TEST(JavaClassGeneratorTest, CommentsForSimpleResourcesArePresent) { EXPECT_THAT(output, HasSubstr(expected_text)); } -TEST(JavaClassGeneratorTest, CommentsForEnumAndFlagAttributesArePresent) {} +TEST(JavaClassGeneratorTest, CommentsForEnumAndFlagAttributesArePresent) { + std::unique_ptr<Attribute> flagAttr = + test::AttributeBuilder() + .SetTypeMask(android::ResTable_map::TYPE_FLAGS) + .SetComment("Flag attribute") + .AddItemWithComment("flagOne", 0x01, "Flag comment 1") + .AddItemWithComment("flagTwo", 0x02, "@deprecated Flag comment 2") + .Build(); + std::unique_ptr<Attribute> enumAttr = + test::AttributeBuilder() + .SetTypeMask(android::ResTable_map::TYPE_ENUM) + .SetComment("Enum attribute") + .AddItemWithComment("enumOne", 0x01, "@TestApi Enum comment 1") + .AddItemWithComment("enumTwo", 0x02, "Enum comment 2") + .Build(); + + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .AddValue("android:attr/one", std::move(flagAttr)) + .AddValue("android:attr/two", std::move(enumAttr)) + .Build(); + + std::unique_ptr<IAaptContext> context = + test::ContextBuilder() + .AddSymbolSource(util::make_unique<ResourceTableSymbolSource>(table.get())) + .SetNameManglerPolicy(NameManglerPolicy{"android"}) + .Build(); + JavaClassGeneratorOptions options; + options.use_final = false; + JavaClassGenerator generator(context.get(), table.get(), options); + + std::string output; + StringOutputStream out(&output); + ASSERT_TRUE(generator.Generate("android", &out)); + out.Flush(); + + // Special annotations from the enum/flag values should NOT generate + // annotations for the attribute value. + EXPECT_THAT(output, Not(HasSubstr("@Deprecated"))); + EXPECT_THAT(output, Not(HasSubstr("@android.annotation.TestApi"))); + + EXPECT_THAT(output, HasSubstr("Flag attribute")); + EXPECT_THAT(output, HasSubstr("flagOne")); + EXPECT_THAT(output, HasSubstr("Flag comment 1")); + EXPECT_THAT(output, HasSubstr("flagTwo")); + EXPECT_THAT(output, HasSubstr("@deprecated Flag comment 2")); + + EXPECT_THAT(output, HasSubstr("Enum attribute")); + EXPECT_THAT(output, HasSubstr("enumOne")); + EXPECT_THAT(output, HasSubstr("@TestApi Enum comment 1")); + EXPECT_THAT(output, HasSubstr("enumTwo")); + EXPECT_THAT(output, HasSubstr("Enum comment 2")); +} TEST(JavaClassGeneratorTest, CommentsForStyleablesAndNestedAttributesArePresent) { Attribute attr; diff --git a/tools/aapt2/test/Builders.cpp b/tools/aapt2/test/Builders.cpp index 65f63dc68e54..b5934e40a2a3 100644 --- a/tools/aapt2/test/Builders.cpp +++ b/tools/aapt2/test/Builders.cpp @@ -177,12 +177,25 @@ AttributeBuilder& AttributeBuilder::SetWeak(bool weak) { return *this; } +AttributeBuilder& AttributeBuilder::SetComment(StringPiece comment) { + attr_->SetComment(comment); + return *this; +} + AttributeBuilder& AttributeBuilder::AddItem(StringPiece name, uint32_t value) { attr_->symbols.push_back( Attribute::Symbol{Reference(ResourceName({}, ResourceType::kId, name)), value}); return *this; } +AttributeBuilder& AttributeBuilder::AddItemWithComment(StringPiece name, uint32_t value, + StringPiece comment) { + Reference ref(ResourceName({}, ResourceType::kId, name)); + ref.SetComment(comment); + attr_->symbols.push_back(Attribute::Symbol{ref, value}); + return *this; +} + std::unique_ptr<Attribute> AttributeBuilder::Build() { return std::move(attr_); } diff --git a/tools/aapt2/test/Builders.h b/tools/aapt2/test/Builders.h index 098535d8526f..9ee44ba6d04c 100644 --- a/tools/aapt2/test/Builders.h +++ b/tools/aapt2/test/Builders.h @@ -116,7 +116,10 @@ class AttributeBuilder { AttributeBuilder(); AttributeBuilder& SetTypeMask(uint32_t typeMask); AttributeBuilder& SetWeak(bool weak); + AttributeBuilder& SetComment(android::StringPiece comment); AttributeBuilder& AddItem(android::StringPiece name, uint32_t value); + AttributeBuilder& AddItemWithComment(android::StringPiece name, uint32_t value, + android::StringPiece comment); std::unique_ptr<Attribute> Build(); private: |